Skip to content
imagecache.module 31.4 KiB
Newer Older
<?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.
 * 
 */

/*********************************************************************************************
 * 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($may_cache) {
  $items = array();
  if ($may_cache) {

    // standard imagecache callback.
    $items[] = array(
      'path' => file_directory_path() .'/imagecache', 
      'callback' => 'imagecache_cache',
      'access' => true,

    // private downloads imagecache callback 
    $items[] = array( 
      'path' => 'system/files/imagecache',
      'callback' => 'imagecache_cache_private',
      'access' => true,
      'type' => MENU_CALLBACK
    );     
  }
  return $items;
}

/**
 * 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.',
    ),
    '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 to an exact set of dimensions, ignoring aspect ratio.',
      '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',
    ),
Darrel O'Pry's avatar
Darrel O'Pry committed
    'imagecache_rotate' => array(
      'name' => 'Rotate',
      'description' => 'Rotate an image.',
      'file' => 'imagecache_actions.inc',
    ),
    'imagecache_sharpen' => array(
      'file' => 'imagecache_actions.inc',
      'arguments' => array('element' => NULL),
    ),
    '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 = unserialize($cache->data);
    }
    else {
      foreach (module_implements('imagecache_actions') as $module) {
        foreach (module_invoke($module, 'imagecache_actions') as $key => $action) {
          $action['module'] = $module;
          if ($action['file']) {
            $action['file'] = drupal_get_path('module', $action['module']) .'/'. $action['file'];
          }
          $actions[$key] = $action;
        };
      }
      uasort($actions, '_imagecache_definitions_sort');
      cache_set('imagecache_actions', 'cache', serialize($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 ($definition && $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 is very similar to the Drupal core function file_create_url(), but
 * handles the case of Clean URLs and public downloads differently however.
 *
 * @param $presetname
 * @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.
 *   This is
function imagecache_create_url($presetname, $filepath, $bypass_browser_cache = FALSE) {
  $path = _imagecache_strip_file_directory($filepath);
  $query = $bypass_browser_cache ? time() : $bypass_browser_cache;
  switch (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC)) {
    case FILE_DOWNLOADS_PUBLIC:
      return url($GLOBALS['base_url'] . '/' . file_directory_path() .'/imagecache/'. $presetname .'/'. $path, $query, NULL, TRUE);
      return url('system/files/imagecache/'. $presetname .'/'. $path, $query, NULL, TRUE);
}

/**
 * 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)) {
    _imagecache_cache($preset, $source);
  } 
  else {
    // if there is a 403 image, display it.
    $accesspath = file_create_path('imagecache/'. $preset .'.403.png');
    if (file_exists($accesspath)) {
      imagecache_transfer($accesspath);
      exit;
    }
    header('HTTP/1.0 403 Forbidden');
}

/**
 * handle request validation and responses to imagecache requests. 
 */
function _imagecache_cache($presetname, $path) {
  if (!$preset = imagecache_preset_by_name($presetname)) {
    // send a 404 if we dont' know of a preset. 
    header('HTTP/1.0 404 Not Found');
  // 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 (file_exists($dst)) {
    imagecache_transfer($dst);
  }  

  $src = $path;
  
  // check if the path to the file exists
  if (!is_file($src)) {
    // check if it is a file in the files dir if we couldn't find it.
    $src = file_create_path($src);
  };

  // If the file doesn exist.. It could be an imagefield preview... 
  if (!is_file($src)) {
    // scan for imagefield previews 
    if (!empty($_SESSION['imagefield'])) {
      foreach ($_SESSION['imagefield'] as $fieldname => $files) {
        foreach ($files as $delta => $file) {
          if ($file['preview'] != $src) continue;
          $dst = tempnam(file_directory_temp(), 'imagecache.preview');
          // 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($dst));
          if (!imagecache_build_derivative($preset['actions'], $file['filepath'], $dst)) {
            // Generate an error if image could not generate.
            watchdog('imagecache', t('Failed generating a preview image from %image using imagecache preset %preset.', array('%image' => $path, '%preset' => $presetname)), WATCHDOG_ERROR);
            header('HTTP/1.0 500 Internal Server Error');
    // if there is a 404 image uploaded for the preset display it.
    $notfoundpath = file_create_path('imagecache/'. $preset['presetname'] .'.404.png');
    if (file_exists($notfoundpath)) {
      imagecache_transfer($notfoundpath);
      exit;
    }
    // otherwise send a 404.
    header('HTTP/1.0 404 Not Found');
  // bail if the requested file isn't an image you can't just summon .php files etc...
  if (!getimagesize($src)) {
    header('HTTP/1.0 403 Forbidden');
    exit;
  }

  $lockfile = file_directory_temp() .'/'. $preset['presetname'] . basename($src);
  if (file_exists($lockfile)) {
    watchdog('imagecache', t('Imagecache already generating: %dst, Lock file: %tmp.', array('%dst' => $dst, '%tmp' => $lockfile)), WATCHDOG_NOTICE);
    // send a response code that will make the browser wait and reload in a 1/2 sec. 
  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)) {
    // Generate an error if image could not generate.
    watchdog('imagecache', t('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;
  } 
  imagecache_transfer($dst);
}
function _imagecache_apply_action($action, &$image) {
  if ($definition = imagecache_action_definition($action['action'])) {
    return call_user_func($action['action'] .'_image', $image, $action['data']);
  }
  // skip undefined actions.. module probably got uninstalled or disabled.
  watchdog('imagecache', t('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 path to file to be transferred.
 * @return <exit>
 */
function imagecache_transfer($path) {
  if (function_exists('mime_content_type')) {
    $mime = mime_content_type($path);
  else {
    $size = getimagesize($path);
    $mime = $size['mime'];
  }
  $headers = array('Content-Type: '. mime_header_encode($mime));
  if ($fileinfo = stat($path)) {
    $headers[] = 'Content-Length: '. $fileinfo[7];
    _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. Most code has been taken from drupal_page_cache_header().
 */
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;
}

/**
 * build an image cache derivative
 * 
 * @param $actions  Array of imagecache actions.
 * @param $src      Path of the source file.
 * @param $dst      Path of the destination file.
 * @param $tmp      Path of the temporary file used for manipulating the image.
 * @return true - derivative generated, false - no derivative generated, null - derivative 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) && !mkdir($dir, 0775, true)) {
    watchdog('imagecache', t('Failed to create imagecache directory: %dir', array('%dir' => $dir)), WATCHDOG_ERROR);
    return false;
  if (!$image = imageapi_image_open($src)) {
    return false; 
  foreach ($actions as $action) {
    if (!empty($action['data'])) {
      // QuickSketch, why do these run first/twice? - dopry.
      if (isset($action['data']['width'])) {
        $width = _imagecache_filter('width', $action['data']['width'], $image->info['width'], $image->info['height']);
      }
      if (isset($action['data']['height'])) {
        $height = _imagecache_filter('height', $action['data']['height'], $image->info['width'], $image->info['height']);
      }
      foreach ($action['data'] as $key => $value) {
        $action['data'][$key] = _imagecache_filter($key, $value, $image->info['width'], $image->info['height'], $width, $height);
    if (!_imagecache_apply_action($action, $image)) {
      watchdog( 'imagecache', t('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)) {
    if (file_exists($dst)) {
      watchdog('imagecache', t('Cached image file already exists. There is an issue with your Rewrite configuration.'), WATCHDOG_ERROR);
    }
    return false;
Darrel O'Pry's avatar
 
Darrel O'Pry committed

  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 hook_imagefield_file().
 */
function imagecache_imagefield_file($op, $file) {
  switch ($op) {
    // Delete imagecache presets when imagecache images are deleted.
    case 'delete': imagecache_image_flush($file['filepath']); break;
    // Create imagecache derivatives when files are saved. 
    case 'save': 
      break; 
  }

}
/**
 * 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[$preset['presetname'] .'_default'] = array(
      'field types' => array('image'),
    );
    $formatters[$preset['presetname'] .'_linked'] = array(
      'label' => t('@preset as link to node', array('@preset' => $preset['presetname'])),
      'field types' => array('image'),
    );
    $formatters[$preset['presetname'] .'_imagelink'] = array(
      'label' => t('@preset as link to image', array('@preset' => $preset['presetname'])),
      'field types' => array('image'),
    );
Darrel O'Pry's avatar
Darrel O'Pry committed
    $formatters[$preset['presetname'] .'_path'] = array(
       'label' => t('path to @preset derivative', array('@preset' => $preset['presetname'])),
       'field types' => array('image'),
    );   
    $formatters[$preset['presetname'] .'_url'] = array(
       'label' => t('url to @preset derivative', array('@preset' => $preset['presetname'])),
       'field types' => array('image'),
    );   
  }
  return $formatters;
}

/**
 * Implementation of hook_field_formatter().
 */
function imagecache_field_formatter($field, $item, $formatter, $node) {
  if (empty($item['fid']) && $field['use_default_image']) {
    $item = $field['default_image']; 
  }
  // Views does not load the file for us, while CCK display fields does.
  if (empty($item['filepath'])) {
    $item = array_merge($item,  _imagefield_file_load($item['fid']));
  // If there is no filepath at this point, do nothing.
  if (empty($item['filepath'])) {
    return;
  }

  $parts =  explode('_', $formatter);
  $style = array_pop($parts);
  $presetname = implode('_', $parts);
 
  $class = "imagecache imagecache-$presetname imagecache-$style imagecache-$formatter";
  if ($preset = imagecache_preset_by_name($presetname)) {
    $item['filepath'] = $item['fid'] == 'upload' ? $item['preview'] : $item['filepath'];
    switch ($style) {
      case 'linked':
        $imagetag =  theme('imagecache', $presetname, $item['filepath'], $item['alt'], $item['title']);
        return l($imagetag, 'node/'. $node->nid, array('class' => $class), null, null, false, true); 
    
      case 'imagelink':
        $original_image_url = file_create_url($item['filepath']);
        $imagetag =  theme('imagecache', $presetname, $item['filepath'], $item['alt'], $item['title']);
        return l($imagetag, $original_image_url, array('class' => $class), null, null, false, true);

      case 'url':
        return imagecache_create_url($presetname, $item['filepath']);

Darrel O'Pry's avatar
Darrel O'Pry committed
      case 'path':
        return imagecache_create_path($presetname, $item['filepath']);

Darrel O'Pry's avatar
Darrel O'Pry committed
        return  theme('imagecache', $presetname, $item['filepath'], $item['alt'], $item['title']);
Darrel O'Pry's avatar
Darrel O'Pry committed
  return '<!-- imagecache formatter preset('. $presetname .') not found! -->';
}

/**
 * Filter key word values such as 'top', 'right', 'center', and also percentages.
 * All returned values are in pixels relative to the passed in height and width.
 */
function _imagecache_filter($key, $value, $current_width, $current_height, $new_width = null, $new_height = null) {
  switch ($key) {
    case 'width':
      $value = _imagecache_percent_filter($value, $current_width);
      break;
    case 'height':
      $value = _imagecache_percent_filter($value, $current_height);
      break;
    case 'xoffset':
      $value = _imagecache_keyword_filter($value, $current_width, $new_width);
      break;
    case 'yoffset':
      $value = _imagecache_keyword_filter($value, $current_height, $new_height);
      break;
  }
  return $value;
}

/**
 * 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.
 *   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', t('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 $namespace
 *   presetname of the derivative you wish to generate a tag for.
 * @param $path
 *   path to the original image you wish to create a derivative image tag for.
 * @param $alt
 *   img tag alternate text
 * @param $title
 *   img tag title text
 * @param attributes
 *   optional drupal attributes array. If attributes is set, the default imagecache classes 
 *   will not be set automatically, you must do this manually.
function theme_imagecache($namespace, $path, $alt = '', $title = '', $attributes = null) {
  // check is_null so people can intentionally pass an empty array of attributes to override
  // the defaults completely... if 
  if (is_null($attributes)) {
    $attributes['class'] = 'imagecache imagecache-'. $namespace;
  } 
  $attributes = drupal_attributes($attributes);
  $imagecache_url = imagecache_create_url($namespace, $path);
  return '<img src="'. $imagecache_url .'" alt="'. check_plain($alt) .'" title="'. check_plain($title) .'" '. $attributes .' />';
/************************************************************************************
 * ImageCache action implementation example in module.
 */
function imagecache_resize_image(&$image, $data) {
  if (!imageapi_image_resize($image, $data['width'], $data['height'])) {
    watchdog('imagecache', t('imagecache_resize_image failed. image: %image, data: %data.', array('%path' => $image, '%data' => print_r($data, true))), WATCHDOG_ERROR);
    return false;
  return true;
}

function imagecache_resize_form($action) {
  $form['width'] = array(
    '#type' => 'textfield',
    '#title' => t('Width'),
    '#default_value' => $action['width'],
    '#description' => t('Enter a width in pixels or as a percentage. i.e. 500 or 80%.'),
  );
  $form['height'] = array(
    '#type' => 'textfield',
    '#title' => t('Height'),
    '#default_value' => $action['height'],
    '#description' => t('Enter a height in pixels or as a percentage. i.e. 500 or 80%.'),
  );
  return $form;
}

function theme_imagecache_resize($element) {
  $data = $element['#value'];
  return 'width: '. $data['width'] .', height: '. $data['height'];
}



/**
 *  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) {
  // 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.
  if ($cache = cache_get('imagecache:presets', 'cache')) {
    $presets = unserialize($cache->data);
  }
  else {
    $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);
    }
    cache_set('imagecache:presets', 'cache', serialize($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'])) {
    db_query('UPDATE {imagecache_preset} SET presetname =\'%s\' WHERE presetid = %d', $preset['presetname'], $preset['presetid']);
  }
  else {
    $preset['presetid'] = db_next_id('{imagecache_preset}_presetid');
    db_query('INSERT INTO {imagecache_preset} (presetid, presetname) VALUES (%d, \'%s\')', $preset['presetid'], $preset['presetname']);
  }

  // Reset presets cache.
  imagecache_preset_flush($preset);
  imagecache_presets(true);
  return $preset;
}

function imagecache_preset_delete($preset) {
  imagecache_preset_flush($preset['presetid']);
  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 id
 *   A preset id.
 */
function imagecache_preset_flush($preset) {
  if (user_access('flush imagecache')) {
    $presetdir = realpath(file_directory_path() .'/imagecache/'. $preset['presetname']);
    if (is_dir($presetdir)) {
      _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) {
    file_delete(imagecache_create_path($preset['presetname'], $path));
  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_save($action) {
  if ($action['actionid']) {
    db_query('UPDATE {imagecache_action} SET weight=%d, data=\'%s\' WHERE actionid=%d', $action['weight'], serialize($action['data']), $action['actionid']);
  }
  else {
    $action['actionid'] = db_next_id('{imagecache_action}_actionid');
    db_query('INSERT INTO {imagecache_action} (actionid, presetid, weight, action, data) VALUES (%d, %d, %d,\'%s\', \'%s\')', $action['actionid'], $action['presetid'], $action['weight'], $action['action'], serialize($action['data']));
  }
  $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);