<?php

/**
 * @file
 * Dynamic image resizer and image cacher.
 *
 * ImageCache allows you to setup presets for image processing.
 * If an ImageCache derivative doesn't exist the web server's
 * rewrite rules will pass the request to Drupal which in turn
 * hands it off to imagecache to dynamically generate the file.
 *
 * To view a derivative image you request a special url containing
 * 'imagecache/<presetname>/path/to/file.ext.
 *
 * Presets can be managed at http://example.com/admin/build/imagecache.
 *
 * To view a derivative image you request a special url containing
 * 'imagecache/<presetname>/path/to/file.ext.
 *
 * If you had a preset names 'thumbnail' and you wanted to see the
 * thumbnail version of  http://example.com/files/path/to/myimage.jpg you
 * would use http://example.com/files/imagecache/thumbnail/path/to/myimage.jpg
 *
 * ImageCache provides formatters for CCK Imagefields and is leveraged by several
 * other modules. ImageCache also relies heavily on ImageAPI for it's image processing.
 * If there are errors with actual image processing look to ImageAPI first.
 *
 * @todo: add watermarking capabilities.
 *
 */

/**
 * Imagecache preset storage constant for user-defined presets in the DB.
 */
define('IMAGECACHE_STORAGE_NORMAL', 0);

/**
 * Imagecache preset storage constant for module-defined presets in code.
 */
define('IMAGECACHE_STORAGE_DEFAULT', 1);

/**
 * Imagecache preset storage constant for user-defined presets that override
 * module-defined presets.
 */
define('IMAGECACHE_STORAGE_OVERRIDE', 2);

/*********************************************************************************************
 * Drupal Hooks
 *********************************************************************************************/

/**
 * Implementation of hook_perm().
 */
function imagecache_perm() {
  $perms =  array('administer imagecache', 'flush imagecache');
  foreach (imagecache_presets() as $preset) {
    $perms[] =  'view imagecache '. $preset['presetname'];
  }
  return $perms;
}

/**
 * Implementation of hook_menu().
 */
function imagecache_menu() {
  $items = array();

  // standard imagecache callback.
  $items[file_directory_path() .'/imagecache'] = array(
    'page callback' => 'imagecache_cache',
    'access callback' => '_imagecache_menu_access_public_files',
    'type' => MENU_CALLBACK
  );
  // private downloads imagecache callback
  $items['system/files/imagecache'] = array(
    'page callback' => 'imagecache_cache_private',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );

  return $items;
}

/**
 * Menu access callback for public file transfers.
 */
function _imagecache_menu_access_public_files() {
  return (FILE_DOWNLOADS_PUBLIC == variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC));
}

/**
 * Implementation of hook_form_FORM_ID_alter.
 *
 * Clear imagecache presets cache on admin/build/modules form.
 */
function imagecache_form_system_modules_alter(&$form, $form_state) {
  imagecache_presets(TRUE);
}

/**
 * Implementation of hook_form_FORM_ID_alter.
 *
 * The file system form is modified to include an extra submit handler, so
 * that imagecache can rebuild the menu after the filesystem path is changed.
 */
function imagecache_form_system_file_system_settings_alter(&$form, &$form_state) {
  $form['#submit'][] = 'imagecache_system_file_system_submit';
}

/**
 * Rebuild menus to ensure we've got the right files directory callback.
 */
function imagecache_system_file_system_submit($form, &$form_state) {
  menu_rebuild();
}


/**
 * Implementation of hook_theme().
 */
function imagecache_theme() {
  $theme = array(
    'imagecache' => array(
      'arguments' => array(
        'namespace' => NULL,
        'path' => NULL,
        'alt' => NULL,
        'title' => NULL,
    )),
    'imagecache_imagelink' => array(
      'arguments' => array(
        'namespace' => NULL,
        'path' => NULL,
        'alt' => NULL,
        'title' => NULL,
        'attributes' => array(),
    )),
    'imagecache_resize' => array(
      'file' => 'imagecache_actions.inc',
      'arguments' => array('element' => NULL),
    ),
    'imagecache_scale' => array(
      'file' => 'imagecache_actions.inc',
      'arguments' => array('element' => NULL),
    ),
    'imagecache_scale_and_crop' => array(
      'file' => 'imagecache_actions.inc',
      'arguments' => array('element' => NULL),
    ),
    'imagecache_deprecated_scale' => array(
      'file' => 'imagecache_actions.inc',
      'arguments' => array('element' => NULL),
    ),
    'imagecache_crop' => array(
      'file' => 'imagecache_actions.inc',
      'arguments' => array('element' => NULL),
    ),
    'imagecache_desaturate' => array(
      'file' => 'imagecache_actions.inc',
      'arguments' => array('element' => NULL),
    ),
    'imagecache_rotate' => array(
      'file' => 'imagecache_actions.inc',
      'arguments' => array('element' => NULL),
    ),
    'imagecache_sharpen' => array(
      'file' => 'imagecache_actions.inc',
      'arguments' => array('element' => NULL),
    ),
  );

  foreach (imagecache_presets() as $preset) {
    $theme['imagecache_formatter_'. $preset['presetname'] .'_default'] = array(
      'arguments' => array('element' => NULL),
      'function' => 'theme_imagecache_formatter_default',
    );
    $theme['imagecache_formatter_'. $preset['presetname'] .'_linked'] = array(
      'arguments' => array('element' => NULL),
      'function' => 'theme_imagecache_formatter_linked',
    );
    $theme['imagecache_formatter_'. $preset['presetname'] .'_imagelink'] = array(
      'arguments' => array('element' => NULL),
      'function' => 'theme_imagecache_formatter_imagelink',
    );
    $theme['imagecache_formatter_'. $preset['presetname'] .'_path'] = array(
      'arguments' => array('element' => NULL),
      'function' => 'theme_imagecache_formatter_path',
    );
    $theme['imagecache_formatter_'. $preset['presetname'] .'_url'] = array(
      'arguments' => array('element' => NULL),
      'function' => 'theme_imagecache_formatter_url',
    );
  }

  return $theme;

}

/**
 * Implementation of hook_imagecache_actions.
 *
 * @return array
 *   An array of information on the actions implemented by a module. The array
 *   contains a sub-array for each action node type, with the machine-readable
 *   action name as the key. Each sub-array has up to 3 attributes. Possible
 *   attributes:
 *
 *     "name": the human-readable name of the action. Required.
 *     "description": a brief description of the action. Required.
 *     "file": the name of the include file the action can be found
 *             in relative to the implementing module's path.
 */
function imagecache_imagecache_actions() {
  $actions = array(
    'imagecache_resize' => array(
      'name' => 'Resize',
      'description' => 'Resize an image to an exact set of dimensions, ignoring aspect ratio.',
      'file' => 'imagecache_actions.inc',
    ),
    'imagecache_scale' => array(
      'name' => 'Scale',
      'description' => 'Resize an image maintaining the original aspect-ratio (only one value necessary).',
      'file' => 'imagecache_actions.inc',
    ),
    'imagecache_deprecated_scale' => array(
      'name' => 'Deprecated Scale',
      'description' => 'Precursor to Scale and Crop. Has inside and outside dimension support. This action will be removed in ImageCache 2.1).',
      'file' => 'imagecache_actions.inc',
    ),
    'imagecache_scale_and_crop' => array(
      'name' => 'Scale And Crop',
      'description' => 'Resize an image while maintaining aspect ratio, then crop it to the specified dimensions.',
      'file' => 'imagecache_actions.inc',
    ),
    'imagecache_crop' => array(
      'name' => 'Crop',
      'description' => 'Crop an image to the rectangle specified by the given offsets and dimensions.',
      'file' => 'imagecache_actions.inc',
    ),
    'imagecache_desaturate' => array(
      'name' => 'Desaturate',
      'description' => 'Convert an image to grey scale.',
      'file' => 'imagecache_actions.inc',
    ),
    'imagecache_rotate' => array(
      'name' => 'Rotate',
      'description' => 'Rotate an image.',
      'file' => 'imagecache_actions.inc',
    ),
    'imagecache_sharpen' => array(
      'name' => 'Sharpen',
      'description' => 'Sharpen an image using unsharp masking.',
      'file' => 'imagecache_actions.inc',
    ),
  );

  return $actions;
}

/**
 * Pull in actions exposed by other modules using hook_imagecache_actions().
 *
 * @param $reset
 *   Boolean flag indicating whether the cached data should be
 *   wiped and recalculated.
 *
 * @return
 *   An array of actions to be used when transforming images.
 */
function imagecache_action_definitions($reset = FALSE) {
  static $actions;
  if (!isset($actions) || $reset) {
    if (!$reset && ($cache = cache_get('imagecache_actions')) && !empty($cache->data)) {
      $actions = $cache->data;
    }
    else {
      foreach (module_implements('imagecache_actions') as $module) {
        foreach (module_invoke($module, 'imagecache_actions') as $key => $action) {
          $action['module'] = $module;
          if (!empty($action['file'])) {
            $action['file'] = drupal_get_path('module', $action['module']) .'/'. $action['file'];
          }
          $actions[$key] = $action;
        };
      }
      uasort($actions, '_imagecache_definitions_sort');
      cache_set('imagecache_actions', $actions);
    }
  }
  return $actions;
}

function _imagecache_definitions_sort($a, $b) {
  $a = $a['name'];
  $b = $b['name'];
  if ($a == $b) {
    return 0;
  }
  return ($a < $b) ? -1 : 1;
}

function imagecache_action_definition($action) {
  static $definition_cache;
  if (!isset($definition_cache[$action])) {
    $definitions = imagecache_action_definitions();
    $definition = (isset($definitions[$action])) ? $definitions[$action] : array();

    if (isset($definition['file'])) {
      require_once($definition['file']);
    }
    $definition_cache[$action] = $definition;
  }
  return $definition_cache[$action];
}

/**
 * Return a URL that points to the location of a derivative of the original
 * image transformed with the given preset.
 *
 * Special care is taken to make this work with the possible combinations of
 * Clean URLs and public/private downloads. For example, when Clean URLs are not
 * available an URL with query should be returned, like
 * http://example.com/?q=files/imagecache/foo.jpg, so that ImageCache is able
 * intercept the request for this file.
 *
 * This code began similarly to Drupal core's function file_create_url(), but
 * handles the case of Clean URLs and public downloads differently however.
 * It also implements hook_file_url_alter() which was added to Drupal 7 and
 * backported to PressFlow 6.x.
 *
 * @param $presetname
 *   String specifying an ImageCache preset name.
 * @param $filepath
 *   String specifying the path to the image file.
 * @param $bypass_browser_cache
 *   A Boolean indicating that the URL for the image should be distinct so that
 *   the visitors browser will not be able to use a previously cached version.
 *   Defaults to FALSE.
 * @param $absolute
 *   A Boolean indicating that the URL should be absolute. Defaults to TRUE.
 */
function imagecache_create_url($presetname, $filepath, $bypass_browser_cache = FALSE, $absolute = TRUE) {
  $args = array(
    'query' => empty($bypass_browser_cache) ? NULL : time(),
    // Little hack to avoid having language_url_rewrite() prefix the path with the
    // language code, but preserve the domain rewriting.
    'language' => (object) array('language' => '', 'domain' => $GLOBALS['language']->domain),
  );
  $file_directory = file_directory_path();

  // Determine the path of the derivative inside the files directory.
  $derivative_path = 'imagecache/'. $presetname .'/'. _imagecache_strip_file_directory($filepath);

  // Then construct a full path and see if anyone wants to alter it.
  $altered_path = $old_path = $file_directory .'/'. $derivative_path;
  drupal_alter('file_url', $altered_path);

  // If any module has altered the path, then return the alteration...
  if ($altered_path != $old_path) {
    // ...but use url() so our $bypass_browser_cache parameter is honored.
    return url($altered_path, $args);
  }

  // It was unchanged so use the download method's prefix.
  $prefix = array(
    FILE_DOWNLOADS_PUBLIC => $file_directory,
    FILE_DOWNLOADS_PRIVATE => 'system/files',
  );
  $path = $prefix[variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC)] .'/'. $derivative_path;

  return url($path, $args + array('absolute' => $absolute));
}

/**
 * Return a file system location that points to the location of a derivative
 * of the original image at @p $path, transformed with the given @p $preset.
 * Keep in mind that the image might not yet exist and won't be created.
 */
function imagecache_create_path($presetname, $path) {
  $path = _imagecache_strip_file_directory($path);
  return file_create_path() .'/imagecache/'. $presetname .'/'. $path;
}

/**
 * Remove a possible leading file directory path from the given path.
 */
function _imagecache_strip_file_directory($path) {
  $dirpath = file_directory_path();
  $dirlen = strlen($dirpath);
  if (substr($path, 0, $dirlen + 1) == $dirpath .'/') {
    $path = substr($path, $dirlen + 1);
  }
  return $path;
}


/**
 * callback for handling public files imagecache requests.
 */
function imagecache_cache() {
  $args = func_get_args();
  $preset = check_plain(array_shift($args));
  $path = implode('/', $args);
  _imagecache_cache($preset, $path);
}

/**
 * callback for handling private files imagecache requests
 */
function imagecache_cache_private() {
  $args = func_get_args();
  $preset = check_plain(array_shift($args));
  $source = implode('/', $args);

  if (user_access('view imagecache '. $preset) && !in_array(-1, module_invoke_all('file_download', $source))) {
    _imagecache_cache($preset, $source);
  }
  else {
    // if there is a 403 image, display it.
    $accesspath = file_create_path('imagecache/'. $preset .'.403.png');
    if (is_file($accesspath)) {
      imagecache_transfer($accesspath);
      exit;
    }
    header('HTTP/1.0 403 Forbidden');
    exit;
  }
}

/**
 * Handle request validation and responses to ImageCache requests.
 *
 * @see imagecache_generate_image() if you're writing code that needs to have
 *   ImageCache generate images but not send them to a browser.
 */
function _imagecache_cache($presetname, $path) {
  if (!$preset = imagecache_preset_by_name($presetname)) {
    // Send a 404 if we don't know of a preset.
    header("HTTP/1.0 404 Not Found");
    exit;
  }

  // umm yeah deliver it early if it is there. especially useful
  // to prevent lock files from being created when delivering private files.
  $dst = imagecache_create_path($preset['presetname'], $path);
  if (is_file($dst)) {
    imagecache_transfer($dst);
  }

  // preserve path for watchdog.
  $src = $path;

  // Check if the path to the file exists.
  if (!is_file($src) && !is_file($src = file_create_path($src))) {
    watchdog('imagecache', '404: Unable to find %image ', array('%image' => $src), WATCHDOG_ERROR);
    header("HTTP/1.0 404 Not Found");
    exit;
  };

  // Bail if the requested file isn't an image you can't request .php files
  // etc...
  if (!getimagesize($src)) {
    watchdog('imagecache', '403: File is not an image %image ', array('%image' => $src), WATCHDOG_ERROR);
    header('HTTP/1.0 403 Forbidden');
    exit;
  }

  $lockfile = file_directory_temp() .'/'. $preset['presetname'] . basename($src);
  if (file_exists($lockfile)) {
    watchdog('imagecache', 'ImageCache already generating: %dst, Lock file: %tmp.', array('%dst' => $dst, '%tmp' => $lockfile), WATCHDOG_NOTICE);
    // 307 Temporary Redirect, to myself. Lets hope the image is done next time around.
    header('Location: '. request_uri(), TRUE, 307);
    exit;
  }
  touch($lockfile);
  // register the shtdown function to clean up lock files. by the time shutdown
  // functions are being called the cwd has changed from document root, to
  // server root so absolute paths must be used for files in shutdown functions.
  register_shutdown_function('file_delete', realpath($lockfile));

  // check if deriv exists... (file was created between apaches request handler and reaching this code)
  // otherwise try to create the derivative.
  if (file_exists($dst) || imagecache_build_derivative($preset['actions'], $src, $dst)) {
    imagecache_transfer($dst);
  }
  // Generate an error if image could not generate.
  watchdog('imagecache', 'Failed generating an image from %image using imagecache preset %preset.', array('%image' => $path, '%preset' => $preset['presetname']), WATCHDOG_ERROR);
  header("HTTP/1.0 500 Internal Server Error");
  exit;
}

/**
 * Apply an action to an image.
 *
 * @param $action
 *   Action array
 * @param $image
 *   Image object
 * @return
 *   Boolean, TRUE indicating success and FALSE failure.
 */
function _imagecache_apply_action($action, &$image) {
  $actions = imagecache_action_definitions();

  if ($definition = imagecache_action_definition($action['action'])) {
    $function = $action['action'] .'_image';
    if (function_exists($function)) {
      return $function($image, $action['data']);
    }
  }
  // skip undefined actions.. module probably got uninstalled or disabled.
  watchdog('imagecache', 'non-existant action %action', array('%action' => $action['action']), WATCHDOG_NOTICE);
  return TRUE;
}

/**
 * Helper function to transfer files from imagecache.
 *
 * Determines MIME type and sets a last modified header.
 *
 * @param $path
 *   String containing the path to file to be transferred.
 * @return
 *   This function does not return. It calls exit().
 */

function imagecache_transfer($path) {
  $size = getimagesize($path);
  $headers = array('Content-Type: '. mime_header_encode($size['mime']));

  if ($fileinfo = stat($path)) {
    $headers[] = 'Content-Length: '. $fileinfo[7];
    $headers[] = 'Expires: ' . gmdate('D, d M Y H:i:s', time() + 1209600) .' GMT';
    $headers[] = 'Cache-Control: max-age=1209600, private, must-revalidate';
    _imagecache_cache_set_cache_headers($fileinfo, $headers);
  }
  file_transfer($path, $headers);
  exit;
}

/**
 * Set file headers that handle "If-Modified-Since" correctly for the
 * given fileinfo.
 *
 * Note that this function may return or may call exit().
 *
 * Most code has been taken from drupal_page_cache_header().
 *
 * @param $fileinfo
 *   Array returned by stat().
 * @param
 *   Array of existing headers.
 * @return
 *   Nothing but beware that this function may not return.
 */
function _imagecache_cache_set_cache_headers($fileinfo, &$headers) {
  // Set default values:
  $last_modified = gmdate('D, d M Y H:i:s', $fileinfo[9]) .' GMT';
  $etag = '"'. md5($last_modified) .'"';

  // See if the client has provided the required HTTP headers:
  $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
                        ? stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE'])
                        : FALSE;
  $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH'])
                    ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH'])
                    : FALSE;

  if ($if_modified_since && $if_none_match
      && $if_none_match == $etag // etag must match
      && $if_modified_since == $last_modified) { // if-modified-since must match
    header('HTTP/1.1 304 Not Modified');
    // All 304 responses must send an etag if the 200 response
    // for the same object contained an etag
    header('Etag: '. $etag);
    // We must also set Last-Modified again, so that we overwrite Drupal's
    // default Last-Modified header with the right one
    header('Last-Modified: '. $last_modified);
    exit;
  }

  // Send appropriate response:
  $headers[] = 'Last-Modified: '. $last_modified;
  $headers[] = 'ETag: '. $etag;
}

/**
 * Create a new image based on an image preset.
 *
 * @param $preset
 *   An image preset array.
 * @param $source
 *   Path of the source file.
 * @param $destination
 *   Path of the destination file.
 * @return
 *   TRUE if an image derivative is generated, FALSE if no image
 *  derivative is generated. NULL if the derivative is being generated.
 */
function imagecache_build_derivative($actions, $src, $dst) {
  // get the folder for the final location of this preset...
  $dir = dirname($dst);

  // Build the destination folder tree if it doesn't already exists.
  if (!file_check_directory($dir, FILE_CREATE_DIRECTORY) && !mkdir($dir, 0775, TRUE)) {
    watchdog('imagecache', 'Failed to create imagecache directory: %dir', array('%dir' => $dir), WATCHDOG_ERROR);
    return FALSE;
  }

  // file_check_directory() has an annoying habit of displaying "directory ...
  // has been created" status messages. To avoid confusing visitors we clear
  // out all the status messages for non-ImageCache admins. This might affect
  // some other messages but errors and warnings should still be displayed.
  if (!user_access('administer imagecache')) {
    drupal_get_messages('status', TRUE);
  }

  // Simply copy the file if there are no actions.
  if (empty($actions)) {
    return file_copy($src, $dst, FILE_EXISTS_REPLACE);
  }

  if (!$image = imageapi_image_open($src)) {
    return FALSE;
  }

  if (file_exists($dst)) {
    watchdog('imagecache', 'Cached image file %dst already exists but is being regenerated. There may be an issue with your rewrite configuration.', array('%dst' => $dst), WATCHDOG_WARNING);
  }

  foreach ($actions as $action) {
    if (!empty($action['data'])) {
      // Make sure the width and height are computed first so they can be used
      // in relative x/yoffsets like 'center' or 'bottom'.
      if (isset($action['data']['width'])) {
        $action['data']['width']   = _imagecache_percent_filter($action['data']['width'], $image->info['width']);
      }
      if (isset($action['data']['height'])) {
        $action['data']['height']  = _imagecache_percent_filter($action['data']['height'], $image->info['height']);
      }
      if (isset($action['data']['xoffset'])) {
        $action['data']['xoffset'] = _imagecache_keyword_filter($action['data']['xoffset'], $image->info['width'], $action['data']['width']);
      }
      if (isset($action['data']['yoffset'])) {
        $action['data']['yoffset'] = _imagecache_keyword_filter($action['data']['yoffset'], $image->info['height'], $action['data']['height']);
      }
    }
    if (!_imagecache_apply_action($action, $image)) {
      watchdog('imagecache', 'action(id:%id): %action failed for %src', array('%id' => $action['actionid'], '%action' => $action['action'], '%src' => $src), WATCHDOG_ERROR);
      return FALSE;
    }
  }

  if (!imageapi_image_close($image, $dst)) {
    watchdog('imagecache', 'There was an error saving the new image file %dst.', array('%dst' => $dst), WATCHDOG_ERROR);
    return FALSE;
  }

  return TRUE;
}

/**
 * Implementation of hook_user().
 */
function imagecache_user($op, &$edit, &$account, $category = NULL) {
  // Flush cached old user picture.
  if ($op == 'update' && !empty($account->picture)) {
    imagecache_image_flush($account->picture);
  }
}

/**
 * Implementation of filefield.module's hook_file_delete().
 *
 * Remove derivative images after the originals are deleted by filefield.
 */
function imagecache_file_delete($file) {
  imagecache_image_flush($file->filepath);
}

/**
 * Implementation of hook_field_formatter_info().
 *
 * imagecache formatters are named as $presetname_$style
 * $style is used to determine how the preset should be rendered.
 * If you are implementing custom imagecache formatters please treat _ as
 * reserved.
 *
 * @todo: move the linking functionality up to imagefield and clean up the default image
 * integration.
 */
function imagecache_field_formatter_info() {
  $formatters = array();
  foreach (imagecache_presets() as $preset) {
    $formatters[$preset['presetname'] .'_default'] = array(
      'label' => t('@preset image', array('@preset' => $preset['presetname'])),
      'field types' => array('image', 'filefield'),
    );
    $formatters[$preset['presetname'] .'_linked'] = array(
      'label' => t('@preset image linked to node', array('@preset' => $preset['presetname'])),
      'field types' => array('image', 'filefield'),
    );
    $formatters[$preset['presetname'] .'_imagelink'] = array(
      'label' => t('@preset image linked to image', array('@preset' => $preset['presetname'])),
      'field types' => array('image', 'filefield'),
    );
    $formatters[$preset['presetname'] .'_path'] = array(
      'label' => t('@preset file path', array('@preset' => $preset['presetname'])),
      'field types' => array('image', 'filefield'),
    );
    $formatters[$preset['presetname'] .'_url'] = array(
      'label' => t('@preset URL', array('@preset' => $preset['presetname'])),
      'field types' => array('image', 'filefield'),
    );
  }
  return $formatters;
}

function theme_imagecache_formatter_default($element) {
  // Inside a view $element may contain NULL data. In that case, just return.
  if (empty($element['#item']['fid'])) {
    return '';
  }

  // Extract the preset name from the formatter name.
  $presetname = substr($element['#formatter'], 0, strrpos($element['#formatter'], '_'));
  $style = 'linked';
  $style = 'default';

  $item = $element['#item'];
  $item['data']['alt'] = isset($item['data']['alt']) ? $item['data']['alt'] : '';
  $item['data']['title'] = isset($item['data']['title']) ? $item['data']['title'] : NULL;

  $class = "imagecache imagecache-$presetname imagecache-$style imagecache-{$element['#formatter']}";
  return theme('imagecache', $presetname, $item['filepath'], $item['data']['alt'], $item['data']['title'], array('class' => $class));
}

function theme_imagecache_formatter_linked($element) {
  // Inside a view $element may contain NULL data. In that case, just return.
  if (empty($element['#item']['fid'])) {
    return '';
  }

  // Extract the preset name from the formatter name.
  $presetname = substr($element['#formatter'], 0, strrpos($element['#formatter'], '_'));
  $style = 'linked';

  $item = $element['#item'];
  $item['data']['alt'] = isset($item['data']['alt']) ? $item['data']['alt'] : '';
  $item['data']['title'] = isset($item['data']['title']) ? $item['data']['title'] : NULL;

  $imagetag = theme('imagecache', $presetname, $item['filepath'], $item['data']['alt'], $item['data']['title']);
  $path = empty($item['nid']) ? '' : 'node/'. $item['nid'];
  $class = "imagecache imagecache-$presetname imagecache-$style imagecache-{$element['#formatter']}";
  return l($imagetag, $path, array('attributes' => array('class' => $class), 'html' => TRUE));
}

function theme_imagecache_formatter_imagelink($element) {
  // Inside a view $element may contain NULL data. In that case, just return.
  if (empty($element['#item']['fid'])) {
    return '';
  }

  // Extract the preset name from the formatter name.
  $presetname = substr($element['#formatter'], 0, strrpos($element['#formatter'], '_'));
  $style = 'imagelink';

  $item = $element['#item'];
  $item['data']['alt'] = isset($item['data']['alt']) ? $item['data']['alt'] : '';
  $item['data']['title'] = isset($item['data']['title']) ? $item['data']['title'] : NULL;

  $imagetag = theme('imagecache', $presetname, $item['filepath'], $item['data']['alt'], $item['data']['title']);
  $path = file_create_url($item['filepath']);
  $class = "imagecache imagecache-$presetname imagecache-$style imagecache-{$element['#formatter']}";
  return l($imagetag, $path, array('attributes' => array('class' => $class), 'html' => TRUE));
}

function theme_imagecache_formatter_path($element) {
  // Inside a view $element may contain NULL data. In that case, just return.
  if (empty($element['#item']['fid'])) {
    return '';
  }

  // Extract the preset name from the formatter name.
  $presetname = substr($element['#formatter'], 0, strrpos($element['#formatter'], '_'));

  return imagecache_create_path($presetname, $element['#item']['filepath']);
}

function theme_imagecache_formatter_url($element) {
  // Inside a view $element may contain NULL data. In that case, just return.
  if (empty($element['#item']['fid'])) {
    return '';
  }

  // Extract the preset name from the formatter name.
  $presetname = substr($element['#formatter'], 0, strrpos($element['#formatter'], '_'));

  return imagecache_create_url($presetname, $element['#item']['filepath']);
}

/**
 * Accept a percentage and return it in pixels.
 */
function _imagecache_percent_filter($value, $current_pixels) {
  if (strpos($value, '%') !== FALSE) {
    $value = str_replace('%', '', $value) * 0.01 * $current_pixels;
  }
  return $value;
}

/**
 * Accept a keyword (center, top, left, etc) and return it as an offset in pixels.
 */
function _imagecache_keyword_filter($value, $current_pixels, $new_pixels) {
  switch ($value) {
    case 'top':
    case 'left':
      $value = 0;
      break;
    case 'bottom':
    case 'right':
      $value = $current_pixels - $new_pixels;
      break;
    case 'center':
      $value = $current_pixels/2 - $new_pixels/2;
      break;
  }
  return $value;
}

/**
 * Recursively delete all files and folders in the specified filepath, then
 * delete the containing folder.
 *
 * Note that this only deletes visible files with write permission.
 *
 * @param string $path
 *   A filepath relative to file_directory_path.
 */
function _imagecache_recursive_delete($path) {
  if (is_file($path) || is_link($path)) {
    unlink($path);
  }
  elseif (is_dir($path)) {
    $d = dir($path);
    while (($entry = $d->read()) !== FALSE) {
      if ($entry == '.' || $entry == '..') continue;
      $entry_path = $path .'/'. $entry;
      _imagecache_recursive_delete($entry_path);
    }
    $d->close();
    rmdir($path);
  }
  else {
    watchdog('imagecache', 'Unknown file type(%path) stat: %stat ',
              array('%path' => $path,  '%stat' => print_r(stat($path),1)), WATCHDOG_ERROR);
  }

}

/**
 * Create and image tag for an imagecache derivative
 *
 * @param $presetname
 *   String with the name of the preset used to generate the derivative image.
 * @param $path
 *   String path to the original image you wish to create a derivative image
 *   tag for.
 * @param $alt
 *   Optional string with alternate text for the img element.
 * @param $title
 *   Optional string with title for the img element.
 * @param $attributes
 *   Optional drupal_attributes() array. If $attributes is an array then the
 *   default imagecache classes will not be set automatically, you must do this
 *   manually.
 * @param $getsize
 *   If set to TRUE, the image's dimension are fetched and added as width/height
 *   attributes.
 * @param $absolute
 *   A Boolean indicating that the URL should be absolute. Defaults to TRUE.
 * @return
 *   HTML img element string.
 */
function theme_imagecache($presetname, $path, $alt = '', $title = '', $attributes = NULL, $getsize = TRUE, $absolute = TRUE) {
  // Check is_null() so people can intentionally pass an empty array of
  // to override the defaults completely.
  if (is_null($attributes)) {
    $attributes = array('class' => 'imagecache imagecache-'. $presetname);
  }
  $ours = array(
    'src' => imagecache_create_url($presetname, $path, FALSE, $absolute),
    'alt' => $alt,
    'title' => $title,
  );
  if ($getsize && ($image = image_get_info(imagecache_create_path($presetname, $path)))) {
    $ours += array('width' => $image['width'], 'height' => $image['height']);
  }

  return '<img' . drupal_attributes($ours + $attributes) . '/>';
}

/**
 * Create a link the original image that wraps the derivative image.
 *
 * @param $presetname
 *   String with the name of the preset used to generate the derivative image.
 * @param $path
 *   String path to the original image you wish to create a derivative image
 *   tag for.
 * @param $alt
 *   Optional string with alternate text for the img element.
 * @param $title
 *   Optional string with title for the img element.
 * @param attributes
 *   Optional drupal_attributes() array for the link.
 * @return
 *   An HTML string.
 */
function theme_imagecache_imagelink($presetname, $path, $alt = '', $title = '', $attributes = NULL) {
  $image = theme('imagecache', $presetname, $path, $alt, $title);
  $original_image_url = file_create_url($path);
  return l($image, $original_image_url, array('absolute' => FALSE, 'html' => TRUE, 'attributes' => $attributes));
}

/**
 * Imagecache JS settings and theme function.
 */
function imagecache_add_js() {
  static $added;
  if (!$added) {
    $added = TRUE;
    drupal_add_js(drupal_get_path('module', 'imagecache') .'/imagecache.js');

    $mode = variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC);
    if ($mode == FILE_DOWNLOADS_PUBLIC) {
      $settings['filesUrl'] = $GLOBALS['base_path'] . file_directory_path();
    }
    elseif ($mode == FILE_DOWNLOADS_PRIVATE) {
      $settings['filesUrl'] = 'system/files';
    }
    $settings['filesDirectory'] = file_directory_path();
    $settings['presets'] = array_keys(imagecache_presets());
    drupal_add_js(array('imagecache' => $settings), 'setting');
  }
}

/**
 *  ImageCache 2.x API
 *
 *  The API for imagecache has changed.  The 2.x API returns more structured
 *  data, has shorter function names, and implements more aggressive metadata
 *  caching.
 *
 */

/**
 * Get an array of all presets and their settings.
 *
 * @param reset
 *   if set to TRUE it will clear the preset cache
 *
 * @return
 *   array of presets array( $preset_id => array('presetid' => integer, 'presetname' => string))
 */
function imagecache_presets($reset = FALSE) {
  static $presets = array();

  // Clear  caches if $reset is TRUE;
  if ($reset) {
    $presets = array();
    cache_clear_all('imagecache:presets', 'cache');

    // Clear the content.module cache (refreshes the list of formatters provided by imagefield.module).
    if (module_exists('content')) {
      content_clear_type_cache();
    }
  }
  // Return presets if the array is populated.
  if (!empty($presets)) {
    return $presets;
  }

  // Grab from cache or build the array. To ensure that the Drupal 5 upgrade
  // path works, we also check whether the presets list is an array.
  if (($cache = cache_get('imagecache:presets', 'cache')) && is_array($cache->data)) {
    $presets = $cache->data;
  }
  else {
    $normal_presets = array();

    $result = db_query('SELECT * FROM {imagecache_preset} ORDER BY presetname');
    while ($preset = db_fetch_array($result)) {
      $presets[$preset['presetid']] = $preset;
      $presets[$preset['presetid']]['actions'] = imagecache_preset_actions($preset);
      $presets[$preset['presetid']]['storage'] = IMAGECACHE_STORAGE_NORMAL;

      // Collect normal preset names so we can skip defaults and mark overrides accordingly
      $normal_presets[$preset['presetname']] = $preset['presetid'];
    }

    // Collect default presets and allow modules to modify them before they
    // are cached.
    $default_presets = module_invoke_all('imagecache_default_presets');
    drupal_alter('imagecache_default_presets', $default_presets);

    // Add in default presets if they don't conflict with any normal presets.
    // Mark normal presets that take the same preset namespace as overrides.
    foreach ($default_presets as $preset) {
      if (!empty($preset['presetname'])) {
        if (!isset($normal_presets[$preset['presetname']])) {
          $preset['storage'] = IMAGECACHE_STORAGE_DEFAULT;
          // Use a string preset identifier
          $preset['presetid'] = $preset['presetname'];
          $presets[$preset['presetname']] = $preset;
        }
        else {
          $presetid = $normal_presets[$preset['presetname']];
          $presets[$presetid]['storage'] = IMAGECACHE_STORAGE_OVERRIDE;
        }
      }
    }

    cache_set('imagecache:presets', $presets);
  }
  return $presets;
}

/**
 * Load a preset by preset_id.
 *
 * @param preset_id
 *   The numeric id of a preset.
 *
 * @return
 *   preset array( 'presetname' => string, 'presetid' => integet)
 *   empty array if preset_id is an invalid preset
 */
function imagecache_preset($preset_id, $reset = FALSE) {
  $presets = imagecache_presets($reset);
  return (isset($presets[$preset_id])) ? $presets[$preset_id] : array();
}

/**
 * Load a preset by name.
 *
 * @param preset_name
 *
 * @return
 *   preset array( 'presetname' => string, 'presetid' => integer)
 *   empty array if preset_name is an invalid preset
 */

function imagecache_preset_by_name($preset_name) {
  static $presets_by_name = array();
  if (!$presets_by_name &&  $presets = imagecache_presets()) {
    foreach ($presets as $preset) {
      $presets_by_name[$preset['presetname']] = $preset;
    }
  }
  return (isset($presets_by_name[$preset_name])) ? $presets_by_name[$preset_name] : array();
}

/**
 * Save an ImageCache preset.
 *
 * @param preset
 *   an imagecache preset array.
 * @return
 *   a preset array.  In the case of a new preset, 'presetid' will be populated.
 */
function imagecache_preset_save($preset) {
  // @todo: CRUD level validation?
  if (isset($preset['presetid']) && is_numeric($preset['presetid'])) {
    drupal_write_record('imagecache_preset', $preset, 'presetid');
  }
  else {
    drupal_write_record('imagecache_preset', $preset);
  }

  // Reset presets cache.
  imagecache_preset_flush($preset);
  imagecache_presets(TRUE);

  // Rebuild Theme Registry
  drupal_rebuild_theme_registry();

  return $preset;
}

function imagecache_preset_delete($preset) {
  imagecache_preset_flush($preset);
  db_query('DELETE FROM {imagecache_action} where presetid = %d', $preset['presetid']);
  db_query('DELETE FROM {imagecache_preset} where presetid = %d', $preset['presetid']);
  imagecache_presets(TRUE);
  return TRUE;
}

function imagecache_preset_actions($preset, $reset = FALSE) {
  static $actions_cache = array();

  if ($reset || empty($actions_cache[$preset['presetid']])) {
    $result = db_query('SELECT * FROM {imagecache_action} where presetid = %d order by weight', $preset['presetid']);
    while ($row = db_fetch_array($result)) {
      $row['data'] = unserialize($row['data']);
      $actions_cache[$preset['presetid']][] = $row;
    }
  }

  return isset($actions_cache[$preset['presetid']]) ? $actions_cache[$preset['presetid']] : array();
}

/**
 * Flush cached media for a preset.
 *
 * @param preset
 *   an imagecache preset array.
 */
function imagecache_preset_flush($preset) {
  if (user_access('flush imagecache')) {
    $presetdir = realpath(file_directory_path() .'/imagecache/'. $preset['presetname']);
    if (is_dir($presetdir)) {
      module_invoke_all('imagecache_preset_flush', $presetdir, $preset);
      _imagecache_recursive_delete($presetdir);
    }
  }
}

/**
 * Clear cached versions of a specific file in all presets.
 * @param $path
 *   The Drupal file path to the original image.
 */
function imagecache_image_flush($path) {
  foreach (imagecache_presets() as $preset) {
    $derivative_path = imagecache_create_path($preset['presetname'], $path);
    module_invoke_all('imagecache_image_flush', $derivative_path, $preset, $path);
    file_delete($derivative_path);
  }
}

function imagecache_action($actionid) {
  static $actions;

  if (!isset($actions[$actionid])) {
    $action = array();

    $result = db_query('SELECT * FROM {imagecache_action} WHERE actionid=%d', $actionid);
    if ($row = db_fetch_array($result)) {
      $action = $row;
      $action['data'] = unserialize($action['data']);

      $definition = imagecache_action_definition($action['action']);
      $action = array_merge($definition, $action);
      $actions[$actionid] = $action;
    }
  }
  return $actions[$actionid];
}

function imagecache_action_load($actionid) {
  return imagecache_action($actionid, TRUE);
}

function imagecache_action_save($action) {
  $definition = imagecache_action_definition($action['action']);
  $action = array_merge($definition, $action);

  // Some actions don't have data. Make an empty one to prevent SQL errors.
  if (!isset($action['data'])) {
    $action['data'] = array();
  }

  if (!empty($action['actionid'])) {
    drupal_write_record('imagecache_action', $action, 'actionid');
  }
  else {
    drupal_write_record('imagecache_action', $action);
  }
  $preset = imagecache_preset($action['presetid']);
  imagecache_preset_flush($preset);
  imagecache_presets(TRUE);
  return $action;
}

function imagecache_action_delete($action) {
  db_query('DELETE FROM {imagecache_action} WHERE actionid=%d', $action['actionid']);
  $preset = imagecache_preset($action['presetid']);
  imagecache_preset_flush($preset);
  imagecache_presets(TRUE);
}

/**
 * Implementation of hook_action_info().
 *
 * Note: These are actions in the Drupal core trigger.module sense, not
 * ImageCache actions.
 */
function imagecache_action_info() {
  $actions = array();

  if (module_exists('filefield')) {
    $actions['imagecache_flush_action'] = array(
      'type' => 'node',
      'description' => t("ImageCache: Flush ALL presets for this node's filefield images"),
      'configurable' => FALSE,
      'hooks' => array(
        'nodeapi' => array('presave', 'delete', 'insert', 'update'),
      )
    );
    $actions['imagecache_generate_all_action'] = array(
      'type' => 'node',
      'description' => t("ImageCache: Generate ALL presets for this node's filefield images"),
      'configurable' => FALSE,
      'hooks' => array(
        'nodeapi' => array('presave', 'insert', 'update'),
      )
    );
    $actions['imagecache_generate_action'] = array(
      'type' => 'node',
      'description' => t("ImageCache: Generate configured preset(s) for this node's filefield images"),
      'configurable' => TRUE,
      'hooks' => array(
        'nodeapi' => array('presave', 'insert', 'update'),
      )
    );
  }

  return $actions;
}

/**
 * Flush all imagecache presets for a given node.
 *
 * @param $node
 *   A node object.
 * @param $context
 *   Contains values from the calling action.
 *
 * @see imagecache_action_info()
 */
function imagecache_flush_action(&$node, $context) {
  $files = imagecache_get_images_in_node($node);
  if (!empty($files)) {
    foreach ($files as $file) {
      imagecache_image_flush($file['filepath']);
    }
  }
}

/**
 * Generate all imagecache presets for the given node.
 *
 * @param $node
 *   A node object.
 * @param $context
 *   Contains values from the calling action.
 *
 * @see imagecache_action_info()
 */
function imagecache_generate_all_action(&$node, $context) {
  $files = imagecache_get_images_in_node($node);
  $presets = imagecache_presets();
  if (!empty($files) && !empty($presets)) {
    foreach ($files as $file) {
      foreach ($presets as $presetname) {
        imagecache_generate_image($presetname['presetname'], $file['filepath']);
      }
    }
  }
}

/**
 * Generate imagecache presets for the given node and presets.
 *
 * @param $node
 *   A node object.
 * @param $context
 *   Contains values from the calling action.
 *
 * @see imagecache_action_info()
 * @see imagecache_generate_action_form()
 */
function imagecache_generate_action(&$node, $context) {
  $files = imagecache_get_images_in_node($node);
  if (!empty($files) && !empty($context['imagecache_presets'])) {
    foreach ($files as $file) {
      foreach ($context['imagecache_presets'] as $presetname) {
        imagecache_generate_image($presetname, $file['filepath']);
      }
    }
  }
}

/**
 * Form for configuring the generate action.
 *
 * @see imagecache_generate_action()
 */
function imagecache_generate_action_form($context) {
  $options = array();
  foreach (imagecache_presets() as $preset) {
    $options[$preset['presetname']] = $preset['presetname'];
  }
  $form['presets'] = array(
    '#type' => 'checkboxes',
    '#options' => $options,
    '#description' => t('Select which imagecache presets will be effected'),
    '#required' => TRUE,
    '#default_value' => isset($context['imagecache_presets']) ? $context['imagecache_presets'] : array(),
  );
  // Filter out false checkboxes: http://drupal.org/node/61760#comment-402631
  $form['array_filter'] = array('#type' => 'value', '#value' => TRUE);
  return $form;
}

/**
 * Generate a derivative image given presetname and filepath.
 *
 * This is a developer friendly version of _imagecache_cache(), it doesn't worry
 * about sending HTTP headers or an image back to the client so it's much
 * simpler.
 *
 * @param $presetname
 *   ImageCache preset array.
 * @param $filepath
 *   String filepath from the files table.
 * @return
 *   A Boolean indicating if the operation succeeded.
 */
function imagecache_generate_image($presetname, $filepath) {
  $preset = imagecache_preset_by_name($presetname);
  if (empty($preset['presetname'])) {
    return FALSE;
  }
  $destination = imagecache_create_path($preset['presetname'], $filepath);
  if (file_exists($destination)) {
    return TRUE;
  }
  return imagecache_build_derivative($preset['actions'], $filepath, $destination);
}

/**
 * Given a node, get all images associated with it.
 *
 * Currently this only works with images stored in filefields.
 *
 * @param $node
 *   Node object.
 * @return
 *   An array of info from the files table.
 */
function imagecache_get_images_in_node(&$node) {
  $files = array();
  if (module_exists('filefield')) {
    $data = filefield_get_node_files($node);
    foreach ($data as $key => $value) {
      if (stristr($value['filemime'], 'image')) {
        $files[$key] = $value;
      }
    }
  }
  return $files;
}

