<?php

/**
 * @file image_resize_filter.module
 *
 * After adding to a text format, this filter will parse the contents of
 * submitted content and automatically scale image files to match the set
 * dimensions of img tags.
 *
 * Image that have been created take on the ownership of the original file.
 * Making so when the primary node is deleted, the images it provided are
 * deleted also.
 */

/**
 * Implements hook_help().
 */
function image_resize_filter_help($path, $arg) {
  switch ($path) {
    case 'admin/help#image_resize_filter':
      $instructions = array(
        t('Visit the <a href="!url">Text formats</a> configuration page. Click "configure" next to the text format you want to enable the image resize filter on.', array('!url' => 'admin/config/content/formats')),
        t('Check the box for "Image resize filter" under the list of filters.'),
        t('<strong>Important</strong>: Re-order your enabled filters under "Filter processing order".') .
        '<p>' . t('If using the Image Resize Filter on the "Filtered HTML" text format, you <strong>must</strong> ensure A) that the &lt;img&gt; tag is in the list of allowed tags and B) The "Image resize filter" is run <strong>before</strong> the "HTML filter".') . '</p>' .
        '<p>' . t('If using the Image Resize Filter with BBCode or some other non-HTML filter, the "Image resize filter" must be run AFTER the BBCode filter.') . '</p>',
        t('Optional. Click the Image resize filter tab underneath "Filter settings" to set additional configuration for the the image resize filter.'),
      );

      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The image resize filter module provides a <a href="!url">filter</a> that makes it easy to resize images, especially when combined with a WYSIWYG editor such as tinyMCE or FCKeditor. Users never have to worry about scaling image sizes again, just insert an image and set it\'s height and width properties in HTML and the image is resized on output. To use the image resize filter, follow these steps.', array('!url' => url('admin/help/filter'))) . '</p>';
      $output .= theme('item_list', array('items' => $instructions, 'type' => 'ol', 'title' => t('Installation and use')));
      return $output;
  }
}

/**
 * Implements hook_filter_info().
 */
function image_resize_filter_filter_info() {
  $filters = array();
  $filters['image_resize_filter'] = array(
    'title' => t('Image resize filter'),
    'process callback' => 'image_resize_filter_process_filter',
    'settings callback' => 'image_resize_filter_form',
    'default settings' => array(
      'link' => 0,
      'link_class' => '',
      'link_rel' => '',
      'image_locations' => array('local'),
    ),
    'weight' => 0,
  );
  return $filters;
}

/**
 * Filter callback function.
 */
function image_resize_filter_process_filter($text, $filter) {
  $settings['link'] = $filter->settings['link'];
  $settings['class'] = $filter->settings['link_class'];
  $settings['rel'] = $filter->settings['link_rel'];
  $settings['image_locations'] = array_filter($filter->settings['image_locations']);
  $images = image_resize_filter_get_images($settings, $text);
  return $images ? image_resize_filter_process_images($images, $text, $settings) : $text;
}

/**
 * Implements hook_file_delete().
 */
function image_resize_filter_file_delete($file) {
  if (isset($file->uri)) {
    image_resize_filter_delete_derivatives($file->uri);
  }
}

/**
 * Implements hook_theme().
 */
function image_resize_filter_theme() {
  return array(
    'image_resize_filter_image' => array(
      'variables' => array('image' => NULL, 'settings' => NULL),
    ),
    'image_resize_filter_form' => array(
      'render element' => 'form',
    ),
  );
}

/**
 * Implements hook_file_download().
 */
function image_resize_filter_file_download($uri) {
  // If this is a resized image, use the same access as the original image.
  $matches = array();
  if (preg_match('/^([a-z0-9\-_]+:\/\/)resize\/(.*)?-\d+x\d+(\.png|\.jpg|\.jpeg|\.gif)$/i', $uri, $matches)) {
    $headers = module_invoke_all('file_download', $matches[1] . $matches[2] . $matches[3]);
    if (in_array(-1, $headers)) {
      return -1;
    }
    if (count($headers)) {
      return array(
        'Content-Type' => file_get_mimetype($uri),
        'Content-Length' => filesize($uri),
      );
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function image_resize_filter_form_system_file_system_settings_alter($form, $form_state) {
  $form['#submit'][] = 'image_resize_filter_file_system_settings_submit';
}

/**
 * Additional #submit handler for the system_file_system_settings form.
 */
function image_resize_filter_file_system_settings_submit($form, $form_state) {
  // Clear filter caches when changing file system information.
  cache_clear_all('*', 'cache_filter');
}

/**
 * The form for configuring the Image Resize Filter.
 */
function image_resize_filter_form($form, &$form_state, $filter, $format, $defaults) {
  $filter->settings += $defaults;

  $settings['image_resize'] = array(
    '#description' => t('The image resize filter will analyze &lt;img&gt; tags and compare the given height and width attributes to the actual file. If the file dimensions are different than those given in the &lt;img&gt; tag, the image will be copied and the src attribute will be updated to point to the resized image.'),
    '#theme' => 'image_resize_filter_form',
    '#format' => $format,
    '#parents' => array('filters', 'image_resize_filter', 'settings'),
  );

  $settings['image_resize']['image_locations'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Resize images stored'),
    '#options' => array('local' => t('Locally'), 'remote' => t('On remote servers (note: this copies <em>all</em> remote images locally)')),
    '#default_value' => $filter->settings['image_locations'],
    '#description' => t('This option will determine which images will be analyzed for &lt;img&gt; tag differences. Enabling resizing of remote images can have performance impacts, as all images in the filtered text needs to be transferred via HTTP each time the filter cache is cleared.'),
  );

  $settings['image_resize']['link'] = array(
    '#type' => 'checkbox',
    '#title' => t('If resized, add a link to the original image.'),
    '#default_value' => $filter->settings['link'],
  );

  $settings['image_resize']['link_class'] = array(
    '#type' => 'textfield',
    '#title' => t('Optionally, give it the class'),
    '#size' => '10',
    '#default_value' => $filter->settings['link_class'],
  );

  $settings['image_resize']['link_rel'] = array(
    '#type' => 'textfield',
    '#title' => t('and/or a rel attribute'),
    '#size' => '10',
    '#default_value' => $filter->settings['link_rel'],
  );

  return $settings;
}

/**
 * Theme callback to theme the Image Resize Filter form.
 */
function theme_image_resize_filter_form($variables) {
  $form = $variables['form'];

  $link = 'link';
  $class = 'link_class';
  $rel = 'link_rel';

  $class_element = ' ';
  $class_element .= '<span class="image-resize-filter-class">';
  $class_element .= check_plain($form[$class]['#title']) .': ';
  $form[$class]['#title'] = NULL;
  $class_element .= drupal_render($form[$class]);
  $class_element .= '</span>';

  $rel_element = ' ';
  $rel_element .= '<span class="image-resize-filter-rel">';
  $rel_element .= check_plain($form[$rel]['#title']) .': ';
  $form[$rel]['#title'] = NULL;
  $rel_element .= drupal_render($form[$rel]);
  $rel_element .= '</span>';

  $link_element = drupal_render($form[$link]);

  $output = '';
  $output .= '<div class="container-inline image-resize-filter-link-options">';
  $output .= $link_element;
  $output .= $class_element;
  $output .= $rel_element;
  $output .= '</div>';

  $form['placeholder'] = array(
    '#type' => 'element',
    '#title' => t('Link to the original'),
    '#title_display' => 'before',
    '#children' => $output,
    '#description' => t('Linking to the original can be helpful to give users a full-size view of the image. Adding the class "thickbox" is helpful if you have installed the <a href="http://drupal.org/project/thickbox">thickbox module</a>. The rel attribute may be useful when used with the <a href="http://drupal.org/project/lightbox2">lightbox2</a> and <a href="http://drupal.org/project/shadowbox">shadowbox</a> modules.'),
  );

  // Add a little bit of JavaScript. Not cached since it's only used here.
  drupal_add_js(drupal_get_path('module', 'image_resize_filter') . '/image_resize_filter.js', array('aggregate' => FALSE));
  return filter_xss_admin($form['#description']) . drupal_render_children($form);
}

/**
 * Parsing function to locate all images in a piece of text that need replacing.
 *
 * @param $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 $text
 *   The text to be updated with the new img src tags.
 */
function image_resize_filter_get_images($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';
    if (strpos($src, $base_path) === 0) {
      $location = 'local';
    }
    elseif (preg_match('/http[s]?:\/\/' . preg_quote($_SERVER['HTTP_HOST'] . $base_path, '/') . '/', $src)) {
      $location = 'local';
    }
    elseif (strpos($src, 'http') === 0) {
      $location = 'remote';
    }

    // If not resizing images in this location, continue on to the next image.
    if (!in_array($location, $settings['image_locations'])) {
      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);

      // Build a list of acceptable language prefixes.
      $lang_codes = '';
      if (array_key_exists('locale-url', variable_get('language_negotiation_language', array())) && variable_get('locale_language_negotiation_url_part', 0) == 0) {
        $languages = language_list();
        $lang_codes = array();
        foreach ($languages as $key => $language) {
          if ($language->prefix) {
            $lang_codes[$key] = preg_quote($language->prefix, '!');
          }
        }
        $lang_codes = $lang_codes ? '((' . implode('|', $lang_codes) . ')/)?' : '';
      }

      // Convert to a public file system URI.
      $directory_path = file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath() . '/';
      if (preg_match('!^' . preg_quote($directory_path, '!') . '!', $local_path)) {
        $local_path = 'public://' . preg_replace('!^' . preg_quote($directory_path, '!') . '!', '', $local_path);
      }
      // Convert to a file system path if using private files.
      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);
    }

    // If this is an Image preset, generate the source image if necessary.
    // Formatted as "uri://styles/[style-name]/[schema-name]/[original-path]".
    $image_style_matches = array();
    $scheme = file_uri_scheme($local_path);
    if (!file_exists($local_path) && preg_match('!^' . $scheme . '://styles/([a-z0-9_\-]+)/([a-z0-9_\-]+)/(.*)$!i', $local_path, $image_style_matches) && function_exists('image_style_path')) {
      $style_name = $image_style_matches[1];
      $original_path = $scheme . '://' . $image_style_matches[3];
      if ($style = image_style_load($style_name)) {
        image_style_create_derivative($style, $original_path, $local_path);
      }
    }

    // If this is a remote image, retrieve it to check its size.
    if ($location == 'remote') {
      // Basic flood prevention on remote images.
      $resize_threshold = variable_get('image_resize_filter_threshold', 10);
      if (!flood_is_allowed('image_resize_filter_remote', $resize_threshold, 120)) {
        drupal_set_message(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_event('image_resize_filter_remote', 120);

      $result = drupal_http_request($src);
      if ($result->code == 200) {
        $tmp_file = drupal_tempnam('temporary://', 'image_resize_filter_');
        $handle = fopen($tmp_file, 'w');
        fwrite($handle, $result->data);
        fclose($handle);
        $local_path = $tmp_file;
      }
    }

    // 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']) && empty($attributes['height'])) {
      $attributes['width'] = $actual_width;
      $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]);
    }
    // If the name extension failed (such as an image generated by a script),
    // See if we can determine an extension by MIME type.
    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;
}

/**
 * Processing function for image resize filter. Replace img src properties
 * with a URL to a resized image.
 *
 * @param $images
 *   An array of image information, detailing images that need to be replaced.
 * @param $text
 *   The original text of the post that needs src tags updated.
 * @param $settings
 *   An array of setting for generating the image tag.
 */
function image_resize_filter_process_images($images, $text, $settings) {
  $search = array();
  $replace = array();

  foreach ($images as $image) {
    // Copy remote images locally.
    if ($image['location'] == 'remote') {
      $local_file_path = 'resize/remote/' . md5(file_get_contents($image['local_path'])) . '-' . $image['expected_size']['width'] . 'x' . $image['expected_size']['height'] . '.'. $image['extension'];
      $image['destination'] = variable_get('file_default_scheme', 'public') . '://' . $local_file_path;
    }
    // Destination and local path are the same if we're just adding attributes.
    elseif (!$image['resize']) {
      $image['destination'] = $image['local_path'];
    }
    else {
      $path_info = image_resize_filter_pathinfo($image['local_path']);
      $local_file_dir = file_uri_target($path_info['dirname']);
      $local_file_path = 'resize/' . ($local_file_dir ? $local_file_dir . '/' : '') . $path_info['filename'] . '-' . $image['expected_size']['width'] . 'x' . $image['expected_size']['height'] . '.' . $path_info['extension'];
      $image['destination'] = $path_info['scheme'] . '://' . $local_file_path;
    }

    if (!file_exists($image['destination'])) {
      // Basic flood prevention of resizing.
      $resize_threshold = variable_get('image_resize_filter_threshold', 10);
      if (!flood_is_allowed('image_resize_filter_resize', $resize_threshold, 120)) {
        drupal_set_message(t('Image resize threshold of @count per minute reached. Some images have not been resized. Resave the content to resize remaining images.', array('@count' => floor($resize_threshold / 2))), 'error', FALSE);
        continue;
      }
      flood_register_event('image_resize_filter_resize', 120);

      // Create the resize directory.
      $directory = dirname($image['destination']);
      file_prepare_directory($directory, FILE_CREATE_DIRECTORY);

      // Move remote images into place if they are already the right size.
      if ($image['location'] == 'remote' && !$image['resize']) {
        $handle = fopen($image['destination'], 'w');
        fwrite($handle, file_get_contents($image['local_path']));
        fclose($handle);
      }
      // Resize the local image if the sizes don't match.
      elseif ($image['resize']) {
        $res = image_load($image['local_path']);
        if ($res) {
          // Image loaded successfully; resize
          image_resize($res, $image['expected_size']['width'], $image['expected_size']['height']);
          image_save($res, $image['destination']);
        } else {
          // Image failed to load - type doesn't match extension or invalid; keep original file
          $handle = fopen($image['destination'], 'w');
          fwrite($handle, file_get_contents($image['local_path']));
          fclose($handle);
        }
      }
      @chmod($image['destination'], 0664);
    }

    // Delete our temporary file if this is a remote image.
    image_resize_filter_delete_temp_file($image['location'], $image['local_path']);

    // Replace the existing image source with the resized image.
    // Set the image we're currently updating in the callback function.
    $search[] = $image['img_tag'];
    $replace[] = image_resize_filter_image_tag($image, $settings);
  }

  return str_replace($search, $replace, $text);
}

/**
 * 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;

  // Set the link properties if necessary.
  $image['link'] = FALSE;
  if ($image['resize'] && $settings['link'] && !$image['has_link']) {
    $image['link'] = array();
    $image['link']['attributes'] = array('href' => $image['original']);
    if (!empty($settings['class'])) {
      $image['link']['attributes']['class'] = $settings['class'];
    }
    if (!empty($settings['rel'])) {
      $image['link']['attributes']['rel'] = $settings['rel'];
    }
    if (!empty($image['attributes']['title'])) {
      $image['link']['attributes']['title'] = $image['attributes']['title'];
    }
  }

  // Theme the output and return.
  return theme('image_resize_filter_image', array('image' => $image, 'settings' => $settings));
}

/**
 * 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 theme_image_resize_filter_image($variables) {
  $image = $variables['image'];

  $output = '<img' . drupal_attributes($image['attributes']) . ' />';
  if ($image['link']) {
    $output = '<a'. drupal_attributes($image['link']['attributes']) . '>' . $output . '</a>';
  }
  return $output;
}

/**
 * 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);
  }
}

/**
 * Delete all generated image when the original file is removed.
 */
function image_resize_filter_delete_derivatives($original_uri) {
  // First delete all derivatives in the saved file location.
  $path_info = image_resize_filter_pathinfo($original_uri);
  $basename = $path_info['filename'];
  $extension = $path_info['extension'];
  $directory = str_replace($path_info['scheme'] . '://', $path_info['scheme'] . '://resize/', $path_info['dirname']);

  // Delete all the derivatives.
  file_scan_directory($directory, '/' . preg_quote($basename, '/') . '-[0-9]+[x][0-9]+\.' . preg_quote($extension, '/') . '/', array('callback' => 'file_unmanaged_delete'));

  // Then work up the directories and delete any empty ones.
  $folders = explode('/', $directory);
  $directories = array();
  $current_directory = '';
  foreach ($folders as $folder) {
    $current_directory .= $folder . '/';
    $directories[] = $current_directory;
  }

  foreach (array_reverse($directories) as $directory) {
    if ($directory == ($path_info['scheme'] . '://')) {
      break;
    }

    $directory_files = file_scan_directory($directory, '/.*/');
    if (empty($directory_files)) {
      @rmdir($directory);
    }
    else {
      break;
    }
  }
}

/**
 * Delete the entire set of cached images.
 */
function image_resize_filter_delete_all() {
  foreach (file_get_stream_wrappers() as $scheme => $stream_wrapper) {
    $directory = $scheme . '://resize';
    file_unmanaged_delete_recursive($directory);
  }
  cache_clear_all('*', 'cache_filter');
}

/**
 * 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']);
  $info['scheme'] = file_uri_scheme($uri);

  if (empty($info['scheme'])) {
    foreach (file_get_stream_wrappers() as $scheme => $stream_wrapper) {
      $scheme_base_path = file_stream_wrapper_get_instance_by_scheme($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;
}
