ImageCache in Core

From: andrew morton <drewish@katherinehouse.com>


---

 includes/common.inc                   |    5 
 includes/image.inc                    |  540 ++++++++++++++++++++++++
 includes/theme.inc                    |   41 ++
 modules/system/image.actions.inc      |  302 +++++++++++++
 modules/system/system.admin_image.inc |  744 +++++++++++++++++++++++++++++++++
 modules/system/system.info            |    2 
 modules/system/system.install         |  175 ++++++++
 modules/system/system.module          |  187 ++++++++
 modules/system/system.test            |  211 +++++++++
 modules/user/user.admin.inc           |   11 
 modules/user/user.module              |   16 +
 modules/user/user.test                |   16 +
 profiles/default/default.profile      |   19 +
 13 files changed, 2256 insertions(+), 13 deletions(-)
 create mode 100644 modules/system/image.actions.inc
 create mode 100644 modules/system/system.admin_image.inc


diff --git includes/common.inc includes/common.inc
index 4e48068..a9457b3 100644
--- includes/common.inc
+++ includes/common.inc
@@ -3585,6 +3585,9 @@ function drupal_common_theme() {
     'image' => array(
       'arguments' => array('path' => NULL, 'alt' => '', 'title' => '', 'attributes' => NULL, 'getsize' => TRUE),
     ),
+    'image_preset' => array(
+      'arguments' => array('preset' => NULL, 'path' => NULL, 'alt' => '', 'title' => '', 'attributes' => NULL, 'getsize' => TRUE),
+    ),
     'breadcrumb' => array(
       'arguments' => array('breadcrumb' => NULL),
     ),
@@ -4239,7 +4242,7 @@ function drupal_flush_all_caches() {
   node_types_rebuild();
   // Don't clear cache_form - in-progress form submissions may break.
   // Ordered so clearing the page cache will always be the last action.
-  $core = array('cache', 'cache_filter', 'cache_registry', 'cache_page');
+  $core = array('cache', 'cache_filter', 'cache_registry', 'cache_page', 'cache_image');
   $cache_tables = array_merge(module_invoke_all('flush_caches'), $core);
   foreach ($cache_tables as $table) {
     cache_clear_all('*', $table, TRUE);
diff --git includes/image.inc includes/image.inc
index 4309ac9..5102435 100644
--- includes/image.inc
+++ includes/image.inc
@@ -384,5 +384,545 @@ function image_save(stdClass $image, $destination = NULL) {
 }
 
 /**
+ * Get an array of all presets and their settings.
+ *
+ * @param $reset
+ *   If set to TRUE the internal preset cache will be cleared.
+ * @return
+ *   Array of presets array($pid => array('id' => integer, 'name' => string)).
+ */
+function image_presets($reset = FALSE) {
+  static $presets = array();
+
+  // Clear  caches if $reset is true;
+  if ($reset) {
+    $presets = array();
+    cache_clear_all('image_presets', 'cache');
+  }
+
+  // Grab from cache or build the array.
+  if ($cache = cache_get('image_presets', 'cache')) {
+    $presets = $cache->data;
+  }
+  else {
+    $result = db_query('SELECT * FROM {image_presets} ORDER BY name');
+    while ($preset = db_fetch_array($result)) {
+      $presets[$preset['name']] = $preset;
+      $presets[$preset['name']]['actions'] = image_preset_actions($preset, $reset);
+    }
+    cache_set('image_presets', $presets);
+  }
+
+  return $presets;
+}
+
+/**
+ * Get an array of image presets suitable for using as select list options.
+ *
+ * @param $include_empty
+ *   If TRUE a <none> option will be inserted in the options array.
+ * @return
+ *   Array of image presets both key and value are set to preset name.
+ */
+function image_preset_options($include_empty = TRUE) {
+  $presets = image_presets();
+  $options = array();
+  if ($include_empty && !empty($presets)) {
+    $options[''] = t('<none>');
+  }
+  $options = array_merge($options, drupal_map_assoc(array_keys($presets)));
+  if (empty($options)) {
+    $options[''] = t('No defined presets');
+  }
+  return $options;
+}
+
+/**
+ * Load a preset by preset name or ID. May be used as a loader for menu items.
+ *
+ * @param $name
+ *   The name of the preset.
+ * @param $pid
+ *   Optional. The numeric id of a preset if the name is not known.
+ * @param $reset
+ *   If TRUE the internal preset cache will be reset.
+ * @return
+ *   An image preset with the format of
+ *   array('name' => array('ipid' => int, 'name' => string, 'actions' => array())).
+ *   If the preset name or ID is not valid, an empty array is returned.
+ */
+function image_preset_load($name = NULL, $pid = NULL, $reset = FALSE) {
+  $presets = image_presets($reset);
+
+  // If retrieving by name.
+  if (isset($name) && isset($presets[$name])) {
+    return $presets[$name];
+  }
+
+  // If retrieving by PID.
+  if (isset($pid)) {
+    foreach ($presets as $name => $preset) {
+      if ($preset['ipid'] == $pid) {
+        return $preset;
+      }
+    }
+  }
+
+  // Otherwise the preset was not found.
+  return FALSE;
+}
+
+/**
+ * Save an image preset.
+ *
+ * @param preset
+ *   An image preset array.
+ * @return
+ *   A preset array. In the case of a new preset, 'ipid' will be populated.
+ */
+function image_preset_save($preset) {
+  if (isset($preset['ipid']) && is_numeric($preset['ipid'])) {
+    // Load the existing preset to make sure we account for renamed presets.
+    $old_preset = image_preset_load(NULL, $preset['ipid']);
+    image_preset_flush($old_preset);
+    drupal_write_record('image_presets', $preset, 'ipid');
+  }
+  else {
+    drupal_write_record('image_presets', $preset);
+  }
+
+  // Reset presets cache.
+  image_presets(TRUE);
+  menu_rebuild();
+
+  return $preset;
+}
+
+/**
+ * Delete an image preset.
+ *
+ * @param preset
+ *   An image preset array.
+ * @return
+ *   TRUE on success.
+ */
+function image_preset_delete($preset) {
+  image_preset_flush($preset);
+  db_delete('image_actions')->condition('ipid', $preset['ipid'])->execute();
+  db_delete('image_presets')->condition('ipid', $preset['ipid'])->execute();
+  image_presets(TRUE);
+  menu_rebuild();
+  return TRUE;
+}
+
+/**
+ * Load all the actions for an image preset.
+ *
+ * @param $preset
+ *   An image preset array.
+ * @param $reset
+ *   If set to TRUE the internal cache image actions will be reset.
+ * @return
+ *   Array of actions associated with specified preset in the format
+ *   array('ipid' => array())
+ *   Returns an empty array if the specified preset has no actions.
+ */
+function image_preset_actions($preset, $reset = FALSE) {
+  static $actions;
+
+  if ($reset || !isset($actions)) {
+    $actions = array();
+    $definitions = image_action_definitions($reset);
+    $result = db_query('SELECT * FROM {image_actions} where ipid = %d ORDER BY weight ASC', $preset['ipid']);
+    while ($action = db_fetch_array($result)) {
+      $action['data'] = unserialize($action['data']);
+      $action = array_merge($definitions[$action['action']], $action);
+      $actions[$preset['ipid']][$action['iaid']] = $action;
+    }
+  }
+
+  return isset($actions[$preset['ipid']]) ? $actions[$preset['ipid']] : array();
+}
+
+/**
+ * Flush cached media for a preset.
+ *
+ * @param $preset
+ *   An image preset.
+ */
+function image_preset_flush($preset) {
+  $preset_directory = realpath(file_directory_path() .'/presets/'. $preset['name']);
+  if (is_dir($preset_directory)) {
+    file_unmanaged_delete_recursive($preset_directory);
+  }
+}
+
+/**
+ * Return a complete URL to an image when using a preset.
+ *
+ * @param $preset_name
+ *   The name of the preset to be used with this image.
+ * @param $path
+ *   The path to the image.
+ * @return
+ *   The complete absolute URL to a preset image.
+ */
+function image_preset_url($preset_name, $path) {
+  $path = _image_strip_file_directory($path);
+  return file_create_url(file_create_path() .'/presets/'. $preset_name .'/'. $path);
+}
+
+/**
+ * Return a relative path to an image when using a preset.
+ *
+ * @param $preset_name
+ *   The name of the preset to be used with this image.
+ * @param $path
+ *   The path to the image.
+ * @param $file_system
+ *   Optional. By default this will return the file system path, which may be
+ *   relative to the Drupal root or an absolute path on the server. If set to
+ *   FALSE, this will return the Drupal path instead, suitable for requesting
+ *   the image.
+ * @return
+ *   The path to an image preset image. If $file_system is TRUE the path may
+ *   be used in an is_file() request. If $file_system is FALSE the path may
+ *   be used in theme('image_preset') or url() requests since it contains a
+ *   Drupal path.
+ */
+function image_preset_path($preset_name, $path, $file_system = TRUE) {
+  $path = _image_strip_file_directory($path);
+  if ($file_system || variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC) {
+    return file_directory_path() . '/presets/' . $preset_name . '/' . $path;
+  }
+  else {
+    return 'system/files/presets/' . $preset_name . '/' . $path;
+  }
+}
+
+/**
+ * Given an image preset and path, generate an image derivative.
+ *
+ * @param $preset_name
+ *   The name of the preset to be used with this image.
+ * @param $path
+ *   The path to the image.
+ * @return
+ *   TRUE if the image is able to be generated successfully or FALSE if unable
+ *   to create the image. This function may return NULL when the image
+ *   generation is in progress but not complete.
+ */
+function image_preset_generate($preset_name, $path) {
+  $default_method = 'system_image_preset_request';
+  $path = _image_strip_file_directory($path);
+  $method = variable_get('image_preset_generation_method', $default_method);
+  $preset = image_preset_load($preset_name);
+
+  if (empty($preset)) {
+    watchdog('image', 'A derivative of the image %path could not be generated because the preset %preset does not exist.', array('%path' => $path, '%preset' => $preset_name), WATCHDOG_ERROR);
+    return FALSE;
+  }
+
+  if (drupal_function_exists($method)) {
+    return $method($preset, $path);
+  }
+  else {
+    return $default_method($preset, $path);
+  }
+}
+
+/**
+ * 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 image_preset_create_derivative($preset, $source, $destination) {
+  // Get the folder for the final location of this preset.
+  $directory = dirname($destination);
+
+  // Build the destination folder tree if it doesn't already exist.
+  if (!file_check_directory($directory, FILE_CREATE_DIRECTORY) && !mkdir($directory, 0775, TRUE)) {
+    watchdog('image', 'Failed to create preset directory: %directory', array('%directory' => $directory), WATCHDOG_ERROR);
+    return FALSE;
+  }
+
+  if (!$image = image_load($source)) {
+    return FALSE;
+  }
+
+  foreach ($preset['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']   = _image_filter_percent($action['data']['width'], $image->info['width']);
+      }
+      if (isset($action['data']['height'])) {
+        $action['data']['height']  = _image_filter_percent($action['data']['height'], $image->info['height']);
+      }
+      if (isset($action['data']['xoffset'])) {
+        $action['data']['xoffset'] = _image_filter_percent($action['data']['xoffset'], $image->info['width'], $action['data']['width']);
+      }
+      if (isset($action['data']['yoffset'])) {
+        $action['data']['yoffset'] = _image_filter_percent($action['data']['yoffset'], $image->info['height'], $action['data']['height']);
+      }
+    }
+    image_action_apply($image, $action);
+  }
+
+  if (!image_save($image, $destination)) {
+    if (file_exists($destination)) {
+      watchdog('image', 'Cached image file %destination already exists. There may be an issue with your rewrite configuration.', array('%destination' => $destination), WATCHDOG_ERROR);
+    }
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+/**
+ * Clear cached versions of a specific file in all presets.
+ *
+ * @param $path
+ *   The Drupal file path to the original image.
+ */
+function image_path_flush($path) {
+  $path = _image_strip_file_directory($path);
+  foreach (image_presets() as $preset) {
+    if ($path = file_create_path('presets/'. $preset['name'] .'/'. $path)) {
+      file_unmanaged_delete($path);
+    }
+  }
+}
+
+/**
+ * Load all image actions from the database.
+ *
+ * @param $reset
+ *   If TRUE, the internal list of actions will be regenerated.
+ * @return
+ *   Array of image actions.
+ */
+function image_actions($reset = FALSE) {
+  static $actions;
+
+  if (!isset($actions) || $reset) {
+    $actions = array();
+
+    // Add database image actions.
+    $result = db_query('SELECT * FROM {image_actions}', NULL, array('fetch' => PDO::FETCH_ASSOC));
+    foreach ($result as $action) {
+      $action['data'] = unserialize($action['data']);
+      $definition = image_action_definition_load($action['action'], $reset);
+      // Do not load actions whose definition cannot be found.
+      if ($definition) {
+        $action = array_merge($definition, $action);
+        $actions[$action['iaid']] = $action;
+      }
+    }
+  }
+
+  return $actions;
+}
+
+/**
+ * Pull in actions exposed by other modules using hook_image_actions().
+ *
+ * @param $toolkit
+ *   Name of the image toolkit to use. Toolkit set by image_get_toolkit()
+ *   will be used if none is specified.
+ * @param $reset
+ *   If TRUE, regenerate the internal cache of action definitions.
+ * @return
+ *   An array of actions to be used when transforming images.
+ */
+function image_action_definitions($toolkit = NULL, $reset = FALSE) {
+  static $actions;
+
+  if (!isset($toolkit)) {
+    $toolkit = image_get_toolkit();
+  }
+
+  if (!isset($actions) || $reset) {
+    if (!$reset && ($cache = cache_get('image_actions')) && !empty($cache->data)) {
+      $actions = $cache->data;
+    }
+    else {
+      $actions = array();
+      foreach (module_implements('image_actions') as $module) {
+        foreach (module_invoke($module, 'image_actions') as $key => $action) {
+          // Ensure the current toolkit supports the action.
+          $action['module'] = $module;
+          $action['action'] = $key;
+          $action['data'] = isset($action['data']) ? $action['data'] : array();
+          $actions[$key] = $action;
+        };
+      }
+      uasort($actions, '_image_actions_definitions_sort');
+      cache_set('image_actions', $actions);
+    }
+  }
+
+  return $actions;
+}
+
+/**
+ * Load the definition for an action.
+ *
+ * The action definition is a set of default values that applies to an action
+ * regardless of user settings. This definition consists of an array containing
+ * at least the following values:
+ *  - action: The unique name for the action being performed. Usually prefixed
+ *    with the name of the module providing the action.
+ *  - module: The module providing the action.
+ *  - description: A description of the action.
+ *
+ * @param $action
+ *   The name of the action definition to load.
+ * @param $reset
+ *   If TRUE, regenerate the internal cache of action definitions.
+ * @return
+ *   An array containing at least the following values.
+ *   array(
+ *    'action' => Unique name of the action being performed
+ *    'module' => Name of module provoding the action
+ *    'description' => A description of the action
+ *   )
+ */
+function image_action_definition_load($action, $reset = FALSE) {
+  static $definition_cache;
+
+  if (!isset($definition_cache[$action]) || $reset) {
+    $definitions = image_action_definitions($reset);
+    $definition = (isset($definitions[$action])) ? $definitions[$action] : array();
+    $definition_cache[$action] = $definition;
+  }
+
+  return isset($definition_cache[$action]) ? $definition_cache[$action] : FALSE;
+}
+
+/**
+ * Load a single image action.
+ *
+ * @param $iaid
+ *   The image action ID.
+ * @return
+ *   An image action array.
+ */
+function image_action_load($iaid) {
+  $actions = image_actions();
+  return isset($actions[$iaid]) ? $actions[$iaid] : FALSE;
+}
+
+/**
+ * Save an image action.
+ *
+ * @param $action
+ *   An image action array.
+ * @return
+ *   An image action array. In the case of a new action 'iaid' will be set.
+ */
+function image_action_save($action) {
+  if (!empty($action['iaid'])) {
+    drupal_write_record('image_actions', $action, 'iaid');
+  }
+  else {
+    drupal_write_record('image_actions', $action);
+  }
+  $preset = image_preset_load(NULL, $action['ipid']);
+  image_preset_flush($preset);
+  image_presets(TRUE);
+  return $action;
+}
+
+/**
+ * Delete an image action.
+ *
+ * @param $action
+ *   An image action array.
+ */
+function image_action_delete($action) {
+  db_delete('image_actions')->condition('iaid', $action['iaid'])->execute();
+  $preset = image_preset_load(NULL, $action['ipid']);
+  image_preset_flush($preset);
+  image_presets(TRUE);
+}
+
+/**
+ * Given an image object and action, perform the action on the file.
+ *
+ * @param $image
+ *   An image object.
+ * @param $action
+ *   An image action array.
+ * @return
+ *   TRUE on success. FALSE if unable to perform action on image.
+ */
+function image_action_apply(&$image, $action) {
+  if (drupal_function_exists($action['function'])) {
+    return call_user_func($action['function'], $image, $action['data']);
+  }
+  return FALSE;
+}
+
+/**
+ * Accept a percentage and return it in pixels.
+ */
+function _image_filter_percent($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 _image_filter_keyword($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;
+}
+
+/**
+ * Remove a possible leading file directory path from the given path.
+ */
+function _image_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;
+}
+
+/**
+ * Internal function for sorting image action definitions through uasort().
+ */
+function _image_actions_definitions_sort($a, $b) {
+  return strcasecmp($a['name'], $b['name']);
+}
+
+
+/**
  * @} End of "defgroup image".
  */
diff --git includes/theme.inc includes/theme.inc
index 98fe7c8..7e280c7 100644
--- includes/theme.inc
+++ includes/theme.inc
@@ -1217,11 +1217,44 @@ function theme_links($links, $attributes = array('class' => 'links')) {
  *   A string containing the image tag.
  */
 function theme_image($path, $alt = '', $title = '', $attributes = NULL, $getsize = TRUE) {
-  if (!$getsize || (is_file($path) && (list($width, $height, $type, $image_attributes) = @getimagesize($path)))) {
-    $attributes = drupal_attributes($attributes);
-    $url = (url($path) == $path) ? $path : (base_path() . $path);
-    return '<img src="' . check_url($url) . '" alt="' . check_plain($alt) . '" title="' . check_plain($title) . '" ' . (isset($image_attributes) ? $image_attributes : '') . $attributes . ' />';
+  if ($getsize && (is_file($path))) {
+    list($width, $height, $type, $image_attributes) = @getimagesize($path);
   }
+
+  $attributes = drupal_attributes($attributes);
+  $url = (url($path) == $path) ? $path : (base_path() . $path);
+  return '<img src="' . check_url($url) . '" alt="' . check_plain($alt) . '" title="' . check_plain($title) . '" ' . (isset($image_attributes) ? $image_attributes : '') . $attributes . ' />';
+}
+
+/**
+ * Return a themed image using a specific image preset.
+ *
+ * @param $preset
+ *   The name of the preset to be used to alter the original image.
+ * @param $path
+ *   The path of the image file relative to the Drupal files directory.
+ *   This function does not work with images outside the files directory nor
+ *   with remotely hosted images.
+ * @param $alt
+ *   The alternative text for text-based browsers.
+ * @param $title
+ *   The title text is displayed when the image is hovered in some popular browsers.
+ * @param $attributes
+ *   Associative array of attributes to be placed in the img tag.
+ * @param $getsize
+ *   If set to TRUE, the image's dimension are fetched and added as width/height attributes.
+ * @return
+ *   A string containing the image tag.
+ */
+function theme_image_preset($preset, $path, $alt = '', $title = '', $attributes = NULL, $getsize = TRUE) {
+  $real_path = image_preset_path($preset, $path);
+  $url_path = image_preset_path($preset, $path, FALSE);
+
+  if (!file_exists($real_path)) {
+    $url_path = image_preset_generate($preset, $path);
+  }
+
+  return theme('image', $url_path, $alt, $title, $attributes, $getsize);
 }
 
 /**
diff --git modules/system/image.actions.inc modules/system/image.actions.inc
new file mode 100644
index 0000000..b33b50e
--- /dev/null
+++ modules/system/image.actions.inc
@@ -0,0 +1,302 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Image actions provided by Drupal core.
+ */
+
+/**
+ * Implementation of hook_image_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.
+ *    - function: The name of the function that will perform this action.
+ *      Required.
+ *    - form: The name of the function providing a configuration form for this
+ *      action. Required.
+ *    - summary: The name of a theme function for outputting a summary of the
+ *      action configuration. Required.
+ *    - file: The name of the include file the action can be found in relative to
+ *      the implementing module's path.
+ *    - file path: The directory path to the include file. If left empty, this
+ *      will default to the implementing module's directory.
+ */
+function system_image_actions() {
+  $actions = array(
+    'system_image_resize' => array(
+      'name' => t('Resize'),
+      'description' => t('Resize an image to an exact set of dimensions, ignoring aspect ratio.'),
+      'function' => 'system_image_resize_image',
+      'form' => 'system_image_resize_form',
+      'summary' => 'system_image_resize_summary',
+    ),
+    'system_image_scale' => array(
+      'name' => t('Scale'),
+      'description' => t('Resize an image maintaining the original aspect-ratio (only one value necessary).'),
+      'function' => 'system_image_scale_image',
+      'form' => 'system_image_scale_form',
+      'summary' => 'system_image_scale_summary',
+    ),
+    'system_image_scale_and_crop' => array(
+      'name' => t('Scale and Crop'),
+      'description' => t('Resize an image maintaining the original aspect-ratio, then crop the center of the image to specific dimensions.'),
+      'function' => 'system_image_scale_and_crop_image',
+      'form' => 'system_image_resize_form',
+      'summary' => 'system_image_resize_summary',
+    ),
+    'system_image_crop' => array(
+      'name' => t('Crop'),
+      'description' => t('Crop an image to the rectangle specified by the given offsets and dimensions.'),
+      'function' => 'system_image_crop_image',
+      'form' => 'system_image_crop_form',
+      'summary' => 'system_image_crop_summary',
+    ),
+    'system_image_desaturate' => array(
+      'name' => t('Desaturate'),
+      'description' => t('Convert an image to grey scale.'),
+      'function' => 'system_image_desaturate_image',
+      'form' => 'system_image_desaturate_form',
+      'summary' => 'system_image_desaturate_summary',
+    ),
+    'system_image_rotate' => array(
+      'name' => t('Rotate'),
+      'description' => t('Rotate an image.'),
+      'function' => 'system_image_rotate_image',
+      'form' => 'system_image_rotate_form',
+      'summary' => 'system_image_rotate_summary',
+    ),
+  );
+
+  return $actions;
+}
+
+/**
+ * Image generation callback. Given a preset and image, request a derivative.
+ *
+ * This function is the default image generation method. It generates individual
+ * images by returning a path for an image that can be used in an <img> tag.
+ * When the browser requests the image at system/image/[preset_name]/[path] the
+ * image is generated if it does not already exist and then served to the browser.
+ * This allows each image to have it's own PHP instance (and memory limit) for
+ * generation of the new images.
+ *
+ * @param $preset
+ *   An image preset array.
+ * @param $path
+ *   The local path of the image to be generated, excluding the files directory.
+ * @return
+ *   TRUE if image derivative already exists. Callback path for image generation
+ *   if derivative does not already exist. Suitale for use in an <img> tag.
+ * @see system_image_preset_generate()
+ * @see image_preset_generate()
+ */
+function system_image_preset_request($preset, $path) {
+  $destination = image_preset_path($preset['name'], $path);
+  $image_url = image_preset_url($preset['name'], $path);
+
+  // If the image already exists return true.
+  if (file_exists($destination)) {
+    return TRUE;
+  }
+
+  // Generate a callback path for an image.
+  $token = drupal_get_token($path);
+  $url = url('system/image/' . $preset['name'] . '/' . $path, array('absolute' => TRUE, 'query' => array('token' => $token)));
+  return $url;
+}
+
+/**
+ * Menu callback. Given a preset and image path, generate a derivative and
+ * serve transfer it to the requesting agent via file_transfer().
+ */
+function system_image_preset_generate() {
+  $args = func_get_args();
+  $preset_name = array_shift($args);
+  $preset = image_preset_load($preset_name);
+  $path = implode('/', $args);
+  $source = file_create_path($path);
+  $destination = image_preset_path($preset['name'], $path);
+
+  if (!$preset || !isset($_GET['token'])) {
+    drupal_access_denied();
+    exit();
+  }
+
+  if ($_GET['token'] != drupal_get_token($path)) {
+    drupal_access_denied();
+    exit;
+  }
+
+  $cid = md5($destination);
+  // Image generation is already in progress.
+  if (cache_get($cid, 'cache_image')) {
+    print 'IN PROGRESS';
+    exit;
+  }
+  
+  cache_set($cid, $destination, 'cache_image');
+  
+  if (image_preset_create_derivative($preset, $source, $destination)) {
+    $image = image_load($destination);
+    cache_clear_all($cid, 'cache_image');
+    file_transfer($image->source, array('Content-type: '. $image->info['mime_type']));
+  }
+  else {
+    cache_clear_all($cid, 'cache_image');
+    print 'FALSE';
+  }
+
+  exit;
+}
+
+/**
+ * Access callback for system/image. Ensure this request is made by Drupal.
+ */
+function system_image_preset_generate_access() {
+  $args = func_get_args();
+  $preset_name = array_shift($args);
+  $path = implode('/', $args);
+
+  return isset($_GET['token']) && drupal_valid_token($_GET['token'], $path);
+}
+
+/**
+ * Default implementation image resize action.
+ *
+ * @param $image
+ *   An image object.
+ * @param $data
+ *   An array of attributes to use when performing the resize action.
+ *   array('width' => int, 'height' => int)
+ * @return
+ *   TRUE on success. FALSE on failure to resize image.
+ * @see image_resize()
+ */
+function system_image_resize_image(&$image, $data) {
+  if (!image_resize($image, $data['width'], $data['height'])) {
+    watchdog('image', 'Image resize failed. image: %image, data: %data.', array('%path' => $image, '%data' => print_r($data, TRUE)), WATCHDOG_ERROR);
+    return FALSE;
+  }
+  return TRUE;
+}
+
+/**
+ * Default implementation of image scale action.
+ *
+ * @param $image
+ *   An image object.
+ * @param $data
+ *   An array of attributes to use when performing the scale action.
+ *   array('width' => int, 'height' => int, 'upscale' => bool)
+ * @return
+ *   TRUE on success. FALSE on failure to scale image.
+ * @see image_scale()
+ */
+function system_image_scale_image(&$image, $data) {
+  // Set impossibly large values if the width and height aren't set.
+  $data['width'] = $data['width'] ? $data['width'] : 9999999;
+  $data['height'] = $data['height'] ? $data['height'] : 9999999;
+  if (!image_scale($image, $data['width'], $data['height'], $data['upscale'])) {
+    watchdog('image', 'Image scale failed. image: %image, data: %data.', array('%path' => $image, '%data' => print_r($data, TRUE)), WATCHDOG_ERROR);
+    return FALSE;
+  }
+  return TRUE;
+}
+
+/**
+ * Default implementation of image crop action.
+ *
+ * @param $image
+ *   An image object.
+ * @param $data
+ *   An array containting attributes to use when preforming the crop action.
+ *   array('xoffset' => int, 'yoffset' => int, 'width' => int, 'height' => int)
+ * @return
+ *   TRUE on success. FALSE on failure to crop image.
+ * @see image_crop()
+ */
+function system_image_crop_image(&$image, $data) {
+  if (!image_crop($image, $data['xoffset'], $data['yoffset'], $data['width'], $data['height'])) {
+    watchdog('image', 'Image crop failed. image: %image, data: %data.', array('%path' => $image, '%data' => print_r($data, TRUE)), WATCHDOG_ERROR);
+    return FALSE;
+  }
+  return TRUE;
+}
+
+/**
+ * Default implementation of image scale and crop action.
+ *
+ * @param $image
+ *   An image object.
+ * @param $data
+ *   An array of attributes to use when performing the scale and cropt action.
+ *   array('width' => int, 'height' => int)
+ * @return
+ *   TRUE on success. FALSE on failure to scale and crop image.
+ * @see image_scale_and_crop()
+ */
+function system_image_scale_and_crop_image(&$image, $data) {
+  if (!image_scale_and_crop($image, $data['width'], $data['height'])) {
+    watchdog('image', t('Image scale and crop failed. image: %image, data: %data.', array('%path' => $image, '%data' => print_r($data, TRUE))), WATCHDOG_ERROR);
+    return FALSE;
+  }
+  return TRUE;
+}
+
+/**
+ * Default implementation of image desaturate action.
+ *
+ * @param $image
+ *   An image object.
+ * @param $data
+ *   An array of attributes to use when performing the desaturate action.
+ * @return
+ *   TRUE on success. FALSE on failure to desaturate image.
+ * @see image_desaturate()
+ */
+function system_image_desaturate_image(&$image, $data) {
+  if (!image_desaturate($image)) {
+    watchdog('image', 'Image desaturate failed. image: %image, data: %data.', array('%path' => $image, '%data' => print_r($data, TRUE)), WATCHDOG_ERROR);
+    return FALSE;
+  }
+  return TRUE;
+}
+
+/**
+ * Default implementation of image rotate action.
+ *
+ * @param $image
+ *   An image object.
+ * @param $data
+ *   An array of attributes to use when performing the rotate action.
+ *   array('degrees' => int, 'random' => bool, 'bgcolor' => string)
+ * @return
+ *   TRUE on success. FALSE on failure to rotate image.
+ * @see image_rotate().
+ */
+function system_image_rotate_image(&$image, $data) {
+  // Set sane default values.
+  $data['degrees'] = $data['degrees'] ? $data['degrees'] : 0;
+  $data['random'] = $data['random'] ? $data['random'] : FALSE;
+  $data['bgcolor'] = $data['bgcolor'] ? $data['bgcolor'] : '#FFFFFF';
+
+  // Manipulate the bgcolor if we need to randomize, and convert to proper colors.
+  $data['bgcolor'] = '0x'. str_replace('#', '', $data['bgcolor']);
+
+  if (!empty($data['random'])) {
+    $degrees = abs((float)$data['degrees']);
+    $data['degrees'] = rand(-1 * $degrees, $degrees);
+  }
+
+  if (!image_rotate($image, $data['degrees'], $data['bgcolor'])) {
+    watchdog('image', t('Image rotate failed. image: %image, data: %data.', array('%path' => $image, '%data' => print_r($data, TRUE))), WATCHDOG_ERROR);
+    return FALSE;
+  }
+  return TRUE;
+}
diff --git modules/system/system.admin_image.inc modules/system/system.admin_image.inc
new file mode 100644
index 0000000..dd3a361
--- /dev/null
+++ modules/system/system.admin_image.inc
@@ -0,0 +1,744 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Administration pages for image settings.
+ */
+
+/**
+ * Menu callback; Listing of all current image presets.
+ */
+function system_image_presets() {
+  $presets = image_presets();
+  return theme('admin_image_presets', $presets);
+}
+
+/**
+ * Menu title callback; Title for editing, deleting, and flushing presets.
+ */
+function system_image_preset_title($string, $preset_name) {
+  $preset = image_preset_load($preset_name);
+  return t($string, array('!name' => $preset['name']));
+}
+
+/**
+ * Form builder; Edit a preset name and action order.
+ *
+ * @ingroup forms
+ * @see system_image_preset_form_submit()
+ * @see system_image_preset_name_validate()
+ */
+function system_image_preset_form($form_state, $preset_name) {
+  $preset = image_preset_load($preset_name);
+  $form = array();
+
+  $form['name'] = array(
+    '#type' => 'textfield',
+    '#size' => '64',
+    '#title' => t('Preset name'),
+    '#default_value' => $preset['name'],
+    '#description' => t('The name is used in URLs for generated images. Only use lowercase alphanumeric characters, underscores (_), and hyphens (-) for preset names.'),
+    '#element_validate' => array('system_image_preset_name_validate'),
+  );
+
+  // No need to show a preview if there are no actions.
+  if (count($preset['actions'])) {
+    $preview_path = file_create_path('imageactions_sample.png');
+    if (!file_exists($preview_path)) {
+      // Copy of the sample image into the files directory allows us to generate previews without
+      // needing special case handling.
+      file_unmanaged_copy('misc/sample.png', 'imageactions_sample.png');
+    }
+    $form['preview'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Preview'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+    );
+    $form['preview']['image'] = array(
+      '#markup' => theme('image_preset', $preset['name'], $preview_path),
+    );
+  }
+
+  $form['ipid'] = array(
+    '#type' => 'value',
+    '#value' => $preset['ipid'],
+  );
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Update preset'),
+  );
+  return $form;
+}
+
+/**
+ * Submit handler for saving an image preset name.
+ */
+function system_image_preset_form_submit($form, &$form_state) {
+  // Update the preset name if it has changed.
+  if ($preset = image_preset_load(NULL, $form_state['values']['ipid'])) {
+    if ($preset['name'] != $form_state['values']['name']) {
+      $preset['name'] = $form_state['values']['name'];
+      image_preset_save($preset);
+      $form_state['redirect'] = 'admin/settings/image-presets/' . $preset['name'];
+    }
+  }
+  
+  menu_rebuild();
+  drupal_set_message('Changes to the preset have been saved.');
+}
+
+/**
+ * Form builder; Edit a preset's actions.
+ *
+ * @ingroup forms
+ * @see system_image_preset_action_form_submit()
+ * @see theme_admin_image_preset_actions()
+ */
+function system_image_preset_actions_form($form_state, $preset_name) {
+  $preset = image_preset_load($preset_name);
+  $form = array();
+
+  $form['ipid'] = array(
+    '#type' => 'value',
+    '#value' => $preset['ipid'],
+  );
+
+  $form['actions'] = array(
+    '#tree' => TRUE,
+    '#type' => 'markup',
+    '#theme' => 'admin_image_preset_actions',
+  );
+
+  foreach ($preset['actions'] as $i => $action) {
+    $action_form['name'] = array(
+      '#markup' => $action['name'],
+    );
+    $action_form['action'] = array(
+      '#type' => 'value',
+      '#value' => $action['action'],
+    );
+    $action_form['iaid'] = array(
+      '#type' => 'value',
+      '#value' => $action['iaid'],
+    );
+    $action_form['ipid'] = array(
+      '#type' => 'value',
+      '#value' => $action['ipid'],
+    );
+
+    $action_form['summary'] = array(
+      '#markup' => theme($action['summary'], $action['data']),
+    );
+    $action_form['data'] = array(
+      '#type' => 'value',
+      '#value' => $action['data'],
+    );
+    $action_form['weight'] = array(
+      '#type' => 'weight',
+      '#default_value' => $action['weight'],
+    );
+    $action_form['configure'] = array(
+      '#markup' => ($action['data']) ? l(t('Configure'), 'admin/settings/image-presets/'. $preset['name'] .'/actions/'. $action['iaid'] ) : 'n/a',
+    );
+    $action_form['remove'] = array(
+      '#markup' => l(t('Delete'), 'admin/settings/image-presets/'. $preset['name'] .'/actions/'. $action['iaid'] .'/delete'),
+    );
+    $form['actions'][$i] = $action_form;
+  }
+
+  $form['new_actions'] = array(
+    '#tree' => TRUE,
+    '#type' => 'fieldset',
+    '#title' => t('Add actions'),
+    '#collapsible' => FALSE,
+    '#theme' => 'admin_image_preset_actions_add',
+    '#preset' => $preset,
+  );
+
+  foreach (image_action_definitions() as $action => $definition) {
+    $form['new_actions'][$action] = array(
+      '#type' => 'value',
+      '#value' => $definition,
+    );
+  }
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Update preset'),
+  );
+  return $form;
+}
+
+/**
+ * Submit handler for saving an image preset action weights.
+ */
+function system_image_preset_actions_form_submit($form, &$form_state) {
+  // Update the individual action weights.
+  foreach ($form_state['values']['actions'] as $new_action) {
+    $action = image_action_load($new_action['iaid']);
+    $action['weight'] = $new_action['weight'];
+    image_action_save($action);
+  }
+
+  menu_rebuild();
+  drupal_set_message('Changes to the preset have been saved.');
+}
+
+/**
+ * Form builder; Form for adding and editing image actions.
+ *
+ * @ingroup forms
+ * @see system_image_preset_add_form_submit()
+ * @see system_image_preset_name_validate()
+ */
+function system_image_preset_add_form($form_state) {
+  $form = array();
+  $form['name'] = array(
+    '#type' => 'textfield',
+    '#size' => '64',
+    '#title' => t('Preset name'),
+    '#default_value' => '',
+    '#description' => t('The name is used in URLs for generated images. Only use lowercase alphanumeric characters, underscores (_), and hyphens (-) for preset names.'),
+    '#element_validate' => array('system_image_preset_name_validate'),
+  );
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Create new preset'),
+  );
+  return $form;
+}
+
+/**
+ * Submit handler for adding a new preset.
+ */
+function  system_image_preset_add_form_submit($form, &$form_state) {
+  $preset = array('name' => $form_state['values']['name']);
+  $preset = image_preset_save($preset);
+  menu_rebuild();
+  drupal_set_message(t('Preset %name was created.', array('%name' => $preset['name'])));
+  $form_state['redirect'] = 'admin/settings/image-presets/'. $preset['name'];
+}
+
+/**
+ * Element validate function to ensure unique, URL safe preset names.
+ */
+function system_image_preset_name_validate($element, $form_state) {
+  // Check for duplicates.
+  $presets = image_presets();
+  if (isset($presets[$element['#value']]) && (!isset($form_state['values']['ipid']) || $presets[$element['#value']]['ipid'] != $form_state['values']['ipid'])) {
+    form_set_error($element['#name'], t('The image preset name %name is already in use.', array('%name' => $element['#value'])));
+  }
+
+  // Check for illegal characters in preset names.
+  if (preg_match('/[^0-9a-z_\-]/', $element['#value'])) {
+    form_set_error($element['#name'], t('Please only use lowercase alphanumeric characters, underscores (_), and hyphens (-) for preset names.'));
+  }
+}
+
+/**
+ * Form builder; Form for deleting an image preset.
+ *
+ * @param $preset
+ *   Name of the preset to delete.
+ *
+ * @ingroup forms
+ * @see system_image_preset_delete_form_submit()
+ */
+function system_image_preset_delete_form($form_state, $preset = '') {
+  if (empty($preset)) {
+    drupal_set_message(t('The specified preset was not found.'), 'error');
+    drupal_goto('admin/settings/image-presets');
+  }
+
+  $preset = image_preset_load($preset);
+
+  $form = array();
+  $form['name'] = array(
+    '#type' => 'value',
+    '#value' => $preset['name'],
+  );
+  $form['ipid'] = array(
+    '#type' => 'value',
+    '#value' => $preset['ipid'],
+  );
+  return confirm_form(
+    $form,
+    t('Are you sure you want to delete the preset %preset?', array('%preset' => $preset['name'])),
+    'admin/settings/image-presets',
+    t('All images that have been generated for this preset will be permanently deleted.'),
+    t('Delete'),  t('Cancel')
+  );
+}
+
+/**
+ * Submit handler to delete an image preset.
+ */
+function system_image_preset_delete_form_submit($form, &$form_state) {
+  $preset = image_preset_load(NULL, $form_state['values']['ipid']);
+  image_preset_delete($preset);
+  menu_rebuild();
+  drupal_set_message(t('Preset %name was deleted.', array('%name' => $preset['name'])));
+  $form_state['redirect'] = 'admin/settings/image-presets';
+}
+
+/**
+ * Form builder; Confirm flushing a preset's cached images.
+ *
+ * @param $preset
+ *  Name of preset to flush.
+ * @ingroup forms
+ * @see system_image_preset_flush_form_submit()
+ */
+function system_image_preset_flush_form(&$form_state, $preset = '') {
+  if (empty($preset)) {
+    drupal_set_message(t('The specified preset was not found.'), 'error');
+    $form_state['redirect'] = 'admin/settings/image-presets';
+  }
+
+  $preset = image_preset_load($preset);
+
+  $form = array();
+  $form['name'] = array(
+    '#type' => 'value',
+    '#value' => $preset['name'],
+  );
+  $form['ipid'] = array(
+    '#type' => 'value',
+    '#value' => $preset['ipid'],
+  );
+  return confirm_form(
+    $form,
+    t('Are you sure you want to flush the %preset preset?', array('%preset' => $preset['name'])),
+    'admin/settings/image-presets',
+    t('This will delete all the generated images for the %preset preset. Regenerating the images may cause a temporary increase in your server\'s load.', array('%preset' => $preset['name'])),
+    t('Flush'),  t('Cancel')
+  );
+}
+
+/**
+ * Submit handler for flushing an image preset's cached files.
+ */
+function system_image_preset_flush_form_submit($form, &$form_state) {
+  $preset = image_preset_load(NULL, $form_state['values']['ipid']);
+  image_preset_flush($preset);
+  drupal_set_message(t('Preset %name was flushed.', array('%name' => $preset['name'])));
+  $form_state['redirect'] = 'admin/settings/image-presets';
+}
+
+/**
+ * Menu title callback; Title for editing, deleting, and adding image actions.
+ *
+ * @param $action
+ *   An image action array.
+ */
+function system_image_action_title($string, $action) {
+  if (!is_array($action)) {
+    $action = image_action_load($action);
+  }
+  return t($string, array('!name' => $action['name']));
+}
+
+/**
+ * Form builder; Form for adding and editing image actions.
+ *
+ * This form is used universally for editing all actions. Each action adds its
+ * own custom section to the form by calling the form function specified in
+ * hook_image_actions().
+ *
+ * @ingroup forms
+ * @see hook_image_actions()
+ * @see system_image_actions()
+ * @see system_image_resize_form()
+ * @see system_image_scale_form()
+ * @see system_image_rotate_form()
+ * @see system_image_crop_form()
+ * @see system_image_desaturate_form()
+ * @see system_image_action_form_submit()
+ */
+function system_image_action_form($form_state, $preset_name, $action = '') {
+  $preset = image_preset_load($preset_name);
+  if (!is_array($action)) {
+    $action = image_action_load($action);
+  }
+  
+  $form = array(
+    '#tree' => TRUE,
+    '#image_preset' => $preset,
+    '#image_action' => $action,
+  );
+
+  if (drupal_function_exists($action['form'])) {
+    $form['data'] = call_user_func($action['form'], $action['data']);
+  }
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => isset($action['iaid']) ? t('Update action') : t('Add action'),
+  );
+  return $form;
+}
+
+/**
+ * Submit handler for updating an image action.
+ */
+function system_image_action_form_submit($form, &$form_state) {
+  $preset = $form['#image_preset'];
+  $action = array_merge($form['#image_action'], $form_state['values']);
+  $action['ipid'] = $preset['ipid'];
+  if (empty($action['iaid'])) {
+    $action['weight'] = count($preset['actions']);
+  }
+
+  image_action_save($action);
+  menu_rebuild();
+  drupal_set_message(t('The action was successfully updated.'));
+  $form_state['redirect'] = 'admin/settings/image-presets/'. $preset['name'] .'/actions';
+}
+
+/**
+ * Form builder; Form for deleting an image action.
+ *
+ * @params $preset
+ *   Name of the preset to remove the action from.
+ * @params $action
+ *   Name of the action to remove.
+ * @ingroup forms
+ * @see system_image_action_delete_form_submit()
+ */
+function system_image_action_delete_form($form_state, $preset, $action) {
+  $preset = image_preset_load($preset);
+  $action = image_action_load($action);
+  
+  $form = array(
+    '#image_preset' => $preset,
+    '#image_action' => $action,
+  );
+
+  $question = t('Are you sure you want to delete the @action action from the %preset preset?', array('%preset' => $preset['name'], '@action' => $action['name']));
+  $description = t('Deleting this action will regenerate all images for the %preset preset.', array('%preset' => $preset['name']));
+  return confirm_form($form, $question, 'admin/settings/images/presets/' . $preset['name'], $description, t('Delete'));
+}
+
+/**
+ * Submit handler to delete an image action.
+ */
+function system_image_action_delete_form_submit($form, &$form_state) {
+  $preset = $form['#image_preset'];
+  $action = $form['#image_action'];
+
+  image_action_delete($action);
+  menu_rebuild();
+  drupal_set_message(t('The image action %name has been deleted.', array('%name' => $action['name'])));
+  $form_state['redirect'] = 'admin/settings/image-presets/'. $preset['name'] .'/actions';
+}
+
+/**
+ * Form structure for the image resize form.
+ *
+ * Note that this is not a complete form, it only contains the portion of the
+ * form for configuring the resize options. Therefor it does not not need to
+ * include metadata about the action, nor a submit button.
+ *
+ * @param $data
+ *   The current configuration for this resize action.
+ */
+function system_image_resize_form($data) {
+  $form['width'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Width'),
+    '#default_value' => isset($data['width']) ? $data['width'] : '',
+    '#description' => t('Enter a width in pixels or as a percentage. i.e. 500 or 80%.'),
+    '#required' => TRUE,
+  );
+  $form['height'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Height'),
+    '#default_value' => isset($data['height']) ? $data['height'] : '',
+    '#description' => t('Enter a height in pixels or as a percentage. i.e. 500 or 80%.'),
+    '#required' => TRUE,
+  );
+  return $form;
+}
+
+/**
+ * Form structure for the image scale form.
+ *
+ * Note that this is not a complete form, it only contains the portion of the
+ * form for configuring the scale options. Therefor it does not not need to
+ * include metadata about the action, nor a submit button.
+ *
+ * @param $data
+ *   The current configuration for this scale action.
+ */
+function system_image_scale_form($data) {
+  $form = system_image_resize_form($data);
+  $form['width']['#required'] = FALSE;
+  $form['height']['#required'] = FALSE;
+  $form['upscale'] = array(
+    '#type' => 'checkbox',
+    '#default_value' => (isset($data['upscale'])) ? $data['upscale'] : 0,
+    '#title' => t('Allow Upscaling'),
+    '#description' => t('Let scale make images larger than their original size'),
+  );
+  return $form;
+}
+
+/**
+ * Form structure for the image crop form.
+ *
+ * Note that this is not a complete form, it only contains the portion of the
+ * form for configuring the crop options. Therefor it does not not need to
+ * include metadata about the action, nor a submit button.
+ *
+ * @param $data
+ *   The current configuration for this crop action.
+ */
+function system_image_crop_form($data) {
+  $data += array(
+    'width' => '',
+    'height' => '',
+    'xoffset' => '',
+    'yoffset' => '',
+  );
+
+  $form['width'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Width'),
+    '#default_value' => $data['width'],
+    '#description' => t('Enter a width in pixels or as a percentage. i.e. 500 or 80%.'),
+    '#required' => TRUE,
+  );
+  $form['height'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Height'),
+    '#default_value' => $data['height'],
+    '#description' => t('Enter a height in pixels or as a percentage. i.e. 500 or 80%.'),
+    '#required' => TRUE,
+  );
+  $form['xoffset'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Horizontal offset'),
+    '#default_value' => $data['xoffset'],
+    '#description' => t('Enter a x-axis offset in pixels or use a keyword: <em>left</em>, <em>center</em>, or <em>right</em>.'),
+  );
+  $form['yoffset'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Vertical offset'),
+    '#default_value' => $data['yoffset'],
+    '#description' => t('Enter a y-axis offset in pixels or use a keyword: <em>top</em>, <em>center</em>, or <em>bottom</em>.'),
+  );
+  return $form;
+}
+
+/**
+ * Form structure for the image desaturate form.
+ *
+ * Note that this is not a complete form, it only contains the portion of the
+ * form for configuring the desaturate options. Therefor it does not not need to
+ * include metadata about the action, nor a submit button.
+ *
+ * @param $data
+ *   The current configuration for this crop action.
+ */
+function system_image_desaturate_form($data) {
+  return array(
+    '#markup' => '<div class="messages status">' . t('The desaturate action needs no configuration. Confirm the addition of this action.') . '</div>',
+  );
+}
+
+/**
+ * Form structure for the image rotate form.
+ *
+ * Note that this is not a complete form, it only contains the portion of the
+ * form for configuring the rotate options. Therefor it does not not need to
+ * include metadata about the action, nor a submit button.
+ *
+ * @param $data
+ *   The current configuration for this rotate action.
+ */
+function system_image_rotate_form($data) {
+  $form['degrees'] = array(
+    '#type' => 'textfield',
+    '#default_value' => (isset($data['degrees'])) ? $data['degrees'] : 0,
+    '#title' => t('Rotation angle'),
+    '#description' => t('The number of degrees the image should be rotated. Positive numbers are clockwise, negative are counter-clockwise.'),
+    '#required' => TRUE,
+  );
+  $form['random'] = array(
+    '#type' => 'checkbox',
+    '#default_value' => (isset($data['random'])) ? $data['random'] : 0,
+    '#title' => t('Randomize'),
+    '#description' => t('Randomize the rotation angle for each image. The angle specified above is used as a maximum.'),
+  );
+  $form['bgcolor'] = array(
+    '#type' => 'textfield',
+    '#default_value' => (isset($data['bgcolor'])) ? $data['bgcolor'] : '#FFFFFF',
+    '#title' => t('Background color'),
+    '#description' => t('The background color to use for exposed areas of the image. Use web-style hex colors (#FFFFFF for white, #000000 for black).'),
+    '#required' => TRUE,
+  );
+  return $form;
+}
+
+/**
+ * Display the page containing the list of image presets.
+ *
+ * @param $presets
+ *   An array of all the image presets returned by image_get_presets().
+ * @see image_get_presets()
+ * @ingroup themeable
+ */
+function theme_admin_image_presets($presets) {
+  $header = array(t('Preset name'), array('data' => t('Operations'), 'colspan' => 3));
+  $rows = array();
+  foreach ($presets as $preset) {
+    $row = array();
+    $row[] = l($preset['name'], 'admin/settings/image-presets/'. $preset['name']);
+    $link_attributes = array(
+      'attributes' => array(
+        'class' => 'image-preset-link',
+      ),
+    );
+    $row[] = l(t('Edit'), 'admin/settings/image-presets/'. $preset['name'], $link_attributes);
+    $row[] = l(t('Delete'), 'admin/settings/image-presets/'. $preset['name'] .'/delete', $link_attributes);
+    $row[] = l(t('Flush'), 'admin/settings/image-presets/'. $preset['name'] .'/flush', $link_attributes);
+    $rows[] = $row;
+  }
+
+  if (empty($rows)) {
+    $rows[] = array(array(
+      'colspan' => 4,
+      'data' => t('There are currently no presets. <a href="!url">Add a new one</a>.', array('!url' => url('admin/settings/images/presets/add'))),
+    ));
+  }
+
+  return theme('table', $header, $rows);
+}
+
+/**
+ * Theme callback for listing the actions within a specific image preset.
+ *
+ * @param $form
+ *   An associative array containing the structure of the actions group.
+ * @ingroup themeable
+ */
+function theme_admin_image_preset_actions($form) {
+  $header = array(t('Action'), t('Settings'), t('Weight'), '','');
+  $rows = array();
+  foreach(element_children($form) as $key) {
+    if (!is_numeric($key)) {
+      continue;
+    }
+    $row = array();
+    $form[$key]['weight']['#attributes']['class'] = 'image-action-order-weight';
+    $row[] = drupal_render($form[$key]['name']);
+    $row[] = drupal_render($form[$key]['summary']);
+    $row[] = drupal_render($form[$key]['weight']);
+    $row[] = drupal_render($form[$key]['configure']);
+    $row[] = drupal_render($form[$key]['remove']);
+    $rows[] = array(
+      'data' => $row,
+      'class' => 'draggable',
+    );
+  }
+
+  if (empty($rows)) {
+    $rows[] = array(array(
+      'colspan' => 5,
+      'data' => t('There are currently no actions in this preset. Add one by selecting a preset from the <em>New actions</em> fieldset.'),
+    ));
+  }
+
+  $output = theme('table', $header, $rows, array('id' => 'image-preset-actions'));
+  drupal_add_tabledrag('image-preset-actions', 'order', 'sibling', 'image-action-order-weight');
+  return $output;
+}
+
+/**
+ * Theme callback for adding new actions to a preset.
+ *
+ * @param $form
+ *   An associative array containing the structure of the new actions fieldset.
+ * @ingroup themeable
+ */
+function theme_admin_image_preset_actions_add($form) {
+  $preset = $form['#preset'];
+
+  $output = '<dl class="image-preset-actions-add">';
+  foreach (element_children($form) as $action) {
+    $definition = $form[$action]['#value'];
+    $output .= '<dt>' . l(t('Add !action', array('!action' => $definition['name'])), 'admin/settings/image-presets/' . $preset['name'] . '/add/' . $action) . '</dt>';
+    $output .= '<dd>' . $definition['description'] .'</dd>';
+  }
+  $output .= '</dl>';
+  return $output;
+}
+
+/**
+ * Theme callback for image resize action summary output.
+ *
+ * @param $data
+ *   The current configuration for this resize action.
+ * @ingroup themeable
+ */
+function theme_system_image_resize_summary($data) {
+  return t('Width') . ': '. $data['width'] . ', ' . t('Height') . ': ' . $data['height'];
+}
+
+/**
+ * Theme callback for image scale action summary output.
+ *
+ * @param $data
+ *   The current configuration for this scale action.
+ * @ingroup themeable
+ */
+function theme_system_image_scale_summary($data) {
+  $output = theme('system_image_resize_summary', $data);
+  $output .= ', ' . t('Upscale') . ': ';
+  $output .= $data['upscale'] ? t('Yes') : t('No');
+  return $output;
+}
+
+/**
+ * Theme callback for image crop action summary output.
+ *
+ * @param $data
+ *   The current configuration for this crop action.
+ * @ingroup themeable
+ */
+function theme_system_image_crop_summary($data) {
+  $output = '';
+  $output .= t('Width') . ': ' . ($data['width'] ? $data['width'] : t('auto'));
+  $output .= ', ' . t('Height') . ': ' . ($data['height'] ? $data['height'] : t('auto'));
+  $output .= ', ' . t('X offset') . ': ' . (empty($data['xoffset']) ? 0 : $data['xoffset']);
+  $output .= ', ' . t('Y offset') . ': ' . (empty($data['yoffset']) ? 0 : $data['yoffset']);
+  return $output;
+}
+
+/**
+ * Theme callback for image desaturate action summary output.
+ *
+ * @param $data
+ *   The current configuration for this desaturate action.
+ * @ingroup themeable
+ */
+function theme_system_image_desaturate_summary($data) {
+  return '';
+}
+
+/**
+ * Theme callback for image rotate action summary output.
+ *
+ * @param $data
+ *   The current configuration for this rotate action.
+ * @ingroup themeable
+ */
+function theme_system_image_rotate_summary($data) {
+  $output = t('Degrees: ') . $data['degrees'] .', ';
+  $output .= t('Randomize: ') . (($data['random']) ? t('Yes') : t('No')) .', ';
+  $output .= t('Background: ') . $data['bgcolor'];
+  return $output;
+}
diff --git modules/system/system.info modules/system/system.info
index 7fefb80..9861295 100644
--- modules/system/system.info
+++ modules/system/system.info
@@ -6,7 +6,9 @@ version = VERSION
 core = 7.x
 files[] = system.module
 files[] = system.admin.inc
+files[] = system.admin_image.inc
 files[] = system.queue.inc
 files[] = image.gd.inc
+files[] = image.actions.inc
 files[] = system.install
 required = TRUE
diff --git modules/system/system.install modules/system/system.install
index 36a9910..09a0928 100644
--- modules/system/system.install
+++ modules/system/system.install
@@ -600,6 +600,8 @@ function system_schema() {
     'primary key' => array('cid'),
   );
 
+  $schema['cache_image'] = $schema['cache'];
+  $schema['cache_image']['description'] = 'Cache table used to store information about in progress image manipulations.';
   $schema['cache_form'] = $schema['cache'];
   $schema['cache_form']['description'] = 'Cache table for the form system to store recently built forms and their storage data, to be used in subsequent page requests.';
   $schema['cache_page'] = $schema['cache'];
@@ -737,6 +739,63 @@ function system_schema() {
       'nid' => array('nid'),
     ),
   );
+
+  $schema['image_presets'] = array(
+    'fields' => array(
+      'ipid' => array(
+        'description' => t('The primary identifier for an image preset.'),
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE),
+      'name' => array(
+        'description' => t('The primary identifier for a node.'),
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE),
+    ),
+    'primary key' => array('ipid'),
+    'indexes' => array(
+      'name' => array('name'),
+    ),
+  );
+
+  $schema['image_actions'] = array(
+    'fields' => array(
+      'iaid' => array(
+        'description' => t('The primary identifier for an image action.'),
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE),
+      'ipid' => array(
+        'description' => t('The primary identifier for an image preset.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0),
+      'weight' => array(
+        'description' => t('The weight of the action in the preset.'),
+        'type' => 'int',
+        'unsigned' => FALSE,
+        'not null' => TRUE,
+        'default' => 0),
+      'action' => array(
+        'description' => t('The unique ID of the action to be executed.'),
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE),
+      'data' => array(
+        'description' => t('The configuration data for the action.'),
+        'type' => 'text',
+        'not null' => TRUE,
+        'size' => 'big',
+        'serialize' => TRUE),
+    ),
+    'primary key' => array('iaid'),
+    'indexes' => array(
+      'ipid' => array('ipid'),
+    ),
+  );
+
   $schema['menu_router'] = array(
     'description' => 'Maps paths to various callbacks (access, page and title)',
     'fields' => array(
@@ -3439,6 +3498,122 @@ function system_update_7023() {
 }
 
 /**
+ * Add tables for image presets and actions.
+ */
+function system_update_7024() {
+  $ret = array();
+  $schema = array();
+
+  $schema['cache_image'] = array(
+    'description' => 'Generic cache table for caching things not separated out into their own tables. Contributed modules may also use this to store cached items.',
+    'fields' => array(
+      'cid' => array(
+        'description' => 'Primary Key: Unique cache ID.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'data' => array(
+        'description' => 'A collection of data to cache.',
+        'type' => 'blob',
+        'not null' => FALSE,
+        'size' => 'big',
+      ),
+      'expire' => array(
+        'description' => 'A Unix timestamp indicating when the cache entry should expire, or 0 for never.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'created' => array(
+        'description' => 'A Unix timestamp indicating when the cache entry was created.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'headers' => array(
+        'description' => 'Any custom HTTP headers to be added to cached data.',
+        'type' => 'text',
+        'not null' => FALSE,
+      ),
+      'serialized' => array(
+        'description' => 'A flag to indicate whether content is serialized (1) or not (0).',
+        'type' => 'int',
+        'size' => 'small',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'indexes' => array(
+      'expire' => array('expire'),
+    ),
+    'primary key' => array('cid'),
+  );
+
+  $schema['image_presets'] = array(
+    'fields' => array(
+      'ipid' => array(
+        'description' => t('The primary identifier for an image preset.'),
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE),
+      'name' => array(
+        'description' => t('The primary identifier for a node.'),
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE),
+    ),
+    'primary key' => array('ipid'),
+    'indexes' => array(
+      'name' => array('name'),
+    ),
+  );
+
+  $schema['image_actions'] = array(
+    'fields' => array(
+      'iaid' => array(
+        'description' => t('The primary identifier for an image action.'),
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE),
+      'ipid' => array(
+        'description' => t('The primary identifier for an image preset.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0),
+      'weight' => array(
+        'description' => t('The weight of the action in the preset.'),
+        'type' => 'int',
+        'unsigned' => FALSE,
+        'not null' => TRUE,
+        'default' => 0),
+      'action' => array(
+        'description' => t('The unique ID of the action to be executed.'),
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE),
+      'data' => array(
+        'description' => t('The configuration data for the action.'),
+        'type' => 'text',
+        'not null' => TRUE,
+        'size' => 'big',
+        'serialize' => TRUE),
+    ),
+    'primary key' => array('iaid'),
+    'indexes' => array(
+      'ipid' => array('ipid'),
+    ),
+  );
+
+  db_create_table($ret, 'cache_image', $schema['cache_image']);
+  db_create_table($ret, 'image_presets', $schema['image_presets']);
+  db_create_table($ret, 'image_actions', $schema['image_actions']);
+  return $ret;
+}
+
+/**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
  */
diff --git modules/system/system.module modules/system/system.module
index de806c7..7b6a0cd 100644
--- modules/system/system.module
+++ modules/system/system.module
@@ -155,6 +155,38 @@ function system_theme() {
       'arguments' => array('content' => NULL),
       'file' => 'system.admin.inc',
     ),
+    'admin_image_presets' => array(
+      'arguments' => array('presets' => NULL),
+      'file' => 'system.admin.inc',
+    ),
+    'admin_image_preset_actions' => array(
+      'arguments' => array('form' => NULL),
+      'file' => 'system.admin.inc',
+    ),
+    'admin_image_preset_actions_add' => array(
+      'arguments' => array('form' => NULL),
+      'file' => 'system.admin.inc',
+    ),
+    'system_image_resize_summary' => array(
+      'arguments' => array('data' => NULL),
+      'file' => 'image.actions.inc',
+    ),
+    'system_image_scale_summary' => array(
+      'arguments' => array('data' => NULL),
+      'file' => 'image.actions.inc',
+    ),
+    'system_image_crop_summary' => array(
+      'arguments' => array('data' => NULL),
+      'file' => 'image.actions.inc',
+    ),
+    'system_image_desaturate_summary' => array(
+      'arguments' => array('data' => NULL),
+      'file' => 'image.actions.inc',
+    ),
+    'system_image_rotate_summary' => array(
+      'arguments' => array('data' => NULL),
+      'file' => 'image.actions.inc',
+    ),
     'system_admin_by_module' => array(
       'arguments' => array('menu_items' => NULL),
       'file' => 'system.admin.inc',
@@ -189,6 +221,10 @@ function system_perm() {
       'title' => t('Administer files'),
       'description' => t('Manage user-uploaded files.'),
     ),
+    'administer image presets' => array(
+      'title' => t('Administer image presets'),
+      'description' => t('Create and modify presets for generating image modifications such as thumbnails.'),
+    ),
     'access administration pages' => array(
       'title' => t('Access administration pages'),
       'description' => t('View the administration panel and browse the help system.'),
@@ -461,6 +497,12 @@ function system_menu() {
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
+  $items['system/image'] = array(
+    'title' => 'Generate image preset',
+    'page callback' => 'system_image_preset_generate',
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
   $items['admin'] = array(
     'title' => 'Administer',
     'access arguments' => array('access administration pages'),
@@ -672,6 +714,108 @@ function system_menu() {
     'page arguments' => array('system_file_system_settings'),
     'access arguments' => array('administer site configuration'),
   );
+  $items['admin/settings/image-presets'] = array(
+    'title' => 'Image presets',
+    'description' => 'Configure presets for generating image thumbnails and select an image toolkit.',
+    'page callback' => 'system_image_presets',
+    'access arguments' => array('administer image presets'),
+  );
+  $items['admin/settings/image-presets/list'] = array(
+    'title' => 'List',
+    'description' => 'List the current image presets on the site.',
+    'page callback' => 'system_image_presets',
+    'access arguments' => array('administer image presets'),
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => 1,
+  );
+  $items['admin/settings/image-presets/add'] = array(
+    'title' => 'Add preset',
+    'description' => 'Add a new image preset.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('system_image_preset_add_form'),
+    'access arguments' => array('administer image presets'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 2,
+  );
+  foreach (image_presets() as $preset) {
+    $items['admin/settings/image-presets/'. $preset['name']] = array(
+      'title' => 'Edit preset',
+      'title callback' => 'system_image_preset_title',
+      'title arguments' => array('Edit preset !name', 3),
+      'description' => 'Configure an image preset.',
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('system_image_preset_form', 3),
+      'access arguments' => array('administer image presets'),
+      'type' => MENU_CALLBACK,
+    );
+    $items['admin/settings/image-presets/'. $preset['name'] .'/edit'] = array(
+      'title' => 'Edit',
+      'type' => MENU_DEFAULT_LOCAL_TASK,
+      'weight' => 1,
+    );
+    $items['admin/settings/image-presets/'. $preset['name'] .'/actions'] = array(
+      'title' => 'Actions',
+      'description' => 'Configure an image preset.',
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('system_image_preset_actions_form', 3),
+      'access arguments' => array('administer image presets'),
+      'type' => MENU_LOCAL_TASK,
+      'weight' => 2,
+    );
+    $items['admin/settings/image-presets/'. $preset['name'] .'/delete'] = array(
+      'title' => 'Delete preset',
+      'title callback' => 'system_image_preset_title',
+      'title arguments' => array('Delete preset !name', 3),
+      'description' => 'Configure presets for generating image thumbnails and other manipulations.',
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('system_image_preset_delete_form', 3, TRUE),
+      'access arguments' => array('administer image presets'),
+      'type' => MENU_CALLBACK,
+    );
+    $items['admin/settings/image-presets/'. $preset['name'] .'/flush'] = array(
+      'title' => 'Flush preset',
+      'title callback' => 'system_image_preset_title',
+      'title arguments' => array('Flush preset !name', 3),
+      'description' => 'Flush all the created images for a preset.',
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('system_image_preset_flush_form', 3, TRUE),
+      'access arguments' => array('administer image presets'),
+      'type' => MENU_CALLBACK,
+    );
+    // Cast as an array in case this is called before any actions have
+    // been added, like when a new preset is created.
+    foreach ((array)$preset['actions'] as $action) {
+      $items['admin/settings/image-presets/'. $preset['name'] .'/actions/'. $action['iaid']] = array(
+        'title' => $action['name'],
+        'description' => 'Edit an exiting action within a preset.',
+        'page callback' => 'drupal_get_form',
+        'page arguments' => array('system_image_action_form', 3, 5),
+        'access arguments' => array('administer image presets'),
+        'type' => MENU_LOCAL_TASK,
+        'weight' => $action['weight'],
+      );
+      $items['admin/settings/image-presets/'. $preset['name'] .'/actions/'. $action['iaid'] .'/delete'] = array(
+        'title' => 'Delete image action',
+        'title callback' => 'system_image_action_title',
+        'title arguments' => array('Delete !name action', 5),
+        'description' => 'Delete an exiting action from a preset.',
+        'page callback' => 'drupal_get_form',
+        'page arguments' => array('system_image_action_delete_form', 3, 5),
+        'access arguments' => array('administer image presets'),
+        'type' => MENU_CALLBACK,
+      );
+    }
+    $items['admin/settings/image-presets/'. $preset['name'] .'/add/%image_action_definition'] = array(
+      'title' => 'Add image action',
+      'title callback' => 'system_image_action_title',
+      'title arguments' => array('Add !name action', 5),
+      'description' => 'Add a new action to a preset.',
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('system_image_action_form', 3, 5),
+      'access arguments' => array('administer image presets'),
+      'type' => MENU_CALLBACK,
+    );
+  }
   $items['admin/settings/image-toolkit'] = array(
     'title' => 'Image toolkit',
     'description' => 'Choose which image toolkit to use if you have installed optional toolkits.',
@@ -1114,6 +1258,49 @@ function system_get_files_database(&$files, $type) {
 }
 
 /**
+ * Implementation of hook_file_download().
+ *
+ * Control the access to files underneath the system/files/presets directory.
+ */
+function system_file_download($filepath) {
+  if (strpos($filepath, 'presets/') === 0) {
+    $args = explode('/', $filepath);
+    array_shift($args); // Toss the "presets" item.
+    $preset_name = array_shift($args);
+    $original_path = implode('/', $args);
+
+    // Check that the file exists and is an image.
+    if ($info = image_get_info(file_create_path($filepath))) {
+      // Check the permissions of the original image to grant access to this image.
+      $headers = module_invoke_all('file_download', $original_path);
+      if (!in_array(-1, $headers)) {
+        return array(
+          'Content-Type: ' . $info['mime_type'],
+          'Content-Length: ' . $info['file_size'],
+        );
+      }
+    }
+    return -1;
+  }
+}
+
+/**
+ * Implementation of hook_file_move().
+ */
+function system_file_move($file, $source) {
+  // Delete any image derivatives at the original image path.
+  image_path_flush($file->filepath);
+}
+
+/**
+ * Implementation of hook_file_delete().
+ */
+function system_file_delete($file) {
+  // Delete any image derivatives of this image.
+  image_path_flush($file->filepath);
+}
+
+/**
  * Prepare defaults for themes.
  *
  * @return
diff --git modules/system/system.test modules/system/system.test
index cc8324c..5859283 100644
--- modules/system/system.test
+++ modules/system/system.test
@@ -412,6 +412,217 @@ class AdminMetaTagTestCase extends DrupalWebTestCase {
 }
 
 /**
+ * Tests creation, deletion, and editing of image presets and actions.
+ */
+class AdminImagePresets extends DrupalWebTestCase {
+
+  /**
+   * Implementation of getInfo().
+   */
+  function getInfo() {
+    return array(
+      'name' => t('Image presets and actions configuration'),
+      'description' => t('Tests creation, deletion, and editing of image presets and actions.'),
+      'group' => t('System')
+    );
+  }
+
+  /**
+   * Implementation of setUp().
+   */
+  function setUp() {
+    parent::setUp();
+
+    // Create an administrative user.
+    $this->admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer image presets'));
+    $this->drupalLogin($this->admin_user);
+  }
+
+  /**
+   * Given an image preset, generate an image.
+   */
+  function createSampleImage($preset) {
+    static $file_path;
+
+    // First, we need to make sure we have an image in our testing
+    // file directory. Copy over an image on the first run.
+    if (!isset($file_path)) {
+      $file = reset($this->drupalGetTestFiles('image'));
+      $file_path = file_unmanaged_copy($file->filename);
+    }
+
+    return image_preset_generate($preset['name'], $file_path) ? $file_path : FALSE;
+  }
+
+  /**
+   * Count the number of images currently create for a preset.
+   */
+  function getImageCount($preset) {
+    $directory = file_directory_path() . '/presets/' . $preset['name'];
+    return count(file_scan_directory($directory, '/.*/'));
+  }
+
+  function testPreset() {
+    // Setup a preset to be created and actions to add to it.
+    $preset_name = strtolower($this->randomName(10));
+    $preset_path = 'admin/settings/images/presets/' . $preset_name;
+    $action_edits = array(
+      'system_image_resize' => array(
+        'data[width]' => 100,
+        'data[height]' => 101,
+      ),
+      'system_image_scale' => array(
+        'data[width]' => 110,
+        'data[height]' => 111,
+        'data[upscale]' => 1,
+      ),
+      'system_image_scale_and_crop' => array(
+        'data[width]' => 120,
+        'data[height]' => 121,
+      ),
+      'system_image_crop' => array(
+        'data[width]' => 130,
+        'data[height]' => 131,
+        'data[xoffset]' => 1,
+        'data[yoffset]' => 2,
+      ),
+      'system_image_desaturate' => array(
+        // No options for desaturate.
+      ),
+      'system_image_rotate' => array(
+        'data[degrees]' => 5,
+        'data[random]' => 1,
+        'data[bgcolor]' => '#FFFF00',
+      ),
+    );
+
+    /* Add preset form. */
+
+    $edit = array(
+      'name' => $preset_name,
+    );
+    $this->drupalPost('admin/settings/images/presets/add', $edit, t('Create new preset'));
+    $this->assertRaw(t('Preset %name was created.', array('%name' => $preset_name)), t('Image preset successfully created.'));
+
+    /* Add action form. */
+
+    // Add each sample action to the preset.
+    foreach ($action_edits as $action => $edit) {
+      // Add the action.
+      $this->drupalPost($preset_path . '/add/' . $action, $edit, t('Add action'));
+    }
+
+    /* Edit action form. */
+
+    // Revisit each form to make sure the action was saved.
+    $preset = image_preset_load($preset_name);
+
+    foreach ($preset['actions'] as $iaid => $action) {
+      $this->drupalGet($preset_path . '/' . $iaid);
+      foreach ($action_edits[$action['action']] as $field => $value) {
+        $this->assertFieldByName($field, $value, t('The %field field in the %action action has the correct value of %value.', array('%field' => $field, '%action' => $action['action'], '%value' => $value)));
+      }
+    }
+
+    /* Image preset overview form (ordering and renaming). */
+
+    // Confirm the order of actions is maintained according to the order we
+    // added the fields.
+    $action_edits_order = array_keys($action_edits);
+    $actions_order = array_values($preset['actions']);
+    $order_correct = TRUE;
+    foreach ($actions_order as $index => $action) {
+      if ($action_edits_order[$index] != $action['action']) {
+        $order_correct = FALSE;
+      }
+    }
+    $this->assertTrue($order_correct, t('The order of the actions is correctly set by default.'));
+
+    // Test the preset overview form.
+    // Change the name of the preset and adjust the weights of actions.
+    $preset_name = strtolower($this->randomName(10));
+    $weight = count($action_edits);
+    $edit = array(
+      'name' => $preset_name,
+    );
+    foreach ($preset['actions'] as $iaid => $action) {
+      $edit['actions[' . $iaid . '][weight]'] = $weight;
+      $weight--;
+    }
+
+    // Create an image to make sure it gets flushed after saving.
+    $image_path = $this->createSampleImage($preset);
+    $this->assertEqual($this->getImageCount($preset), 1, t('Image preset %preset image %file successfully generated.', array('%preset' => $preset['name'], '%file' => $image_path)));
+
+    $this->drupalPost($preset_path, $edit, t('Update preset'));
+
+    // Note that after changing the preset name, the preset path is changed.
+    $preset_path = 'admin/settings/images/presets/' . $preset_name;
+
+    // Check that the URL was updated.
+    $this->drupalGet($preset_path);
+    $this->assertResponse(200, t('Image preset %original renamed to %new', array('%original' => $preset['name'], '%new' => $preset_name)));
+
+    // Check that the image was flushed after updating the preset.
+    // This is especially important when renaming the preset. Make sure that
+    // the old image directory has been deleted.
+    $this->assertEqual($this->getImageCount($preset), 0, t('Image preset %preset was flushed after renaming the preset and updating the order of actions.', array('%preset' => $preset['name'])));
+
+    // Load the preset by the new name with the new weights.
+    $preset = image_preset_load($preset_name, NULL, TRUE);
+
+    // Confirm the new preset order was saved.
+    $action_edits_order = array_reverse($action_edits_order);
+    $actions_order = array_values($preset['actions']);
+    $order_correct = TRUE;
+    foreach ($actions_order as $index => $action) {
+      if ($action_edits_order[$index] != $action['action']) {
+        $order_correct = FALSE;
+      }
+    }
+    $this->assertTrue($order_correct, t('The order of the actions is correctly set by default.'));
+
+    /* Image action deletion form. */
+
+    // Create an image to make sure it gets flushed after deleting an action.
+    $image_path = $this->createSampleImage($preset);
+    $this->assertEqual($this->getImageCount($preset), 1, t('Image preset %preset image %file successfully generated.', array('%preset' => $preset['name'], '%file' => $image_path)));
+
+    // Test action deletion form.
+    $action = array_pop($preset['actions']);
+    $this->drupalPost($preset_path . '/' . $action['iaid'] . '/delete', array(), t('Delete'));
+    $this->assertRaw(t('The image action %name has been deleted.', array('%name' => $action['name'])), t('Image action deleted.'));
+
+    // Check that the preset was flushed after deleting an action.
+    $this->assertEqual($this->getImageCount($preset), 0, t('Image preset %preset was flushed after deleting an action.', array('%preset' => $preset['name'])));
+
+    /* Preset flush form. */
+
+    // Create an image to make sure it gets flushed after deleting an action.
+    $image_path = $this->createSampleImage($preset);
+    $this->assertEqual($this->getImageCount($preset), 1, t('Image preset %preset image %file successfully generated.', array('%preset' => $preset['name'], '%file' => $image_path)));
+
+    $this->drupalPost($preset_path . '/flush', array(), t('Flush'));
+    $this->assertRaw(t('Preset %name was flushed.', array('%name' => $preset['name'])), t('Image preset %preset flush form submitted.'));
+
+    $this->assertEqual($this->getImageCount($preset), 0, t('Image preset %preset was flushed after submitting the flush form.', array('%preset' => $preset['name'])));
+
+    /* Preset deletion form. */
+
+    // Delete the preset.
+    $this->drupalPost($preset_path . '/delete', array(), t('Delete'));
+
+    // Confirm the preset directory has been removed.
+    $directory = file_directory_path() . '/presets/' . $preset_name;
+    $this->assertFalse(is_dir($directory), t('Image preset %preset directory removed on preset deletion.', array('%preset' => $preset['name'])));
+
+    // Confirm the preset is no longer available.
+    $this->assertFalse(image_preset_load($preset_name), t('Image preset %preset successfully deleted.', array('%preset' => $preset['name'])));
+
+  }
+}
+
+/**
  * Tests custom access denied functionality.
  */
 class AccessDeniedTestCase extends DrupalWebTestCase {
diff --git modules/user/user.admin.inc modules/user/user.admin.inc
index b6b5397..3c7057c 100644
--- modules/user/user.admin.inc
+++ modules/user/user.admin.inc
@@ -517,10 +517,17 @@ function user_admin_settings() {
     '#maxlength' => 255,
     '#description' => t('URL of picture to display for users with no custom picture selected. Leave blank for none.'),
   );
+  $form['pictures']['settings']['user_picture_preset'] = array(
+    '#type' => 'select',
+    '#title' => t('Upload picture preset'),
+    '#options' => image_preset_options(),
+    '#default_value' => variable_get('user_picture_preset', ''),
+    '#description' => t('Select an image preset to scale user images down without modifying the original image. Image presets may be set up in the <a href="!url">Image handling</a> administration area.', array('!url' => url('admin/settings/images'))),
+  );
   $form['pictures']['settings']['user_picture_dimensions'] = array(
     '#type' => 'textfield',
     '#title' => t('Picture maximum dimensions'),
-    '#default_value' => variable_get('user_picture_dimensions', '85x85'),
+    '#default_value' => variable_get('user_picture_dimensions', '1024x1024'),
     '#size' => 15,
     '#maxlength' => 10,
     '#description' => t('Maximum dimensions for pictures, in pixels.'),
@@ -528,7 +535,7 @@ function user_admin_settings() {
   $form['pictures']['settings']['user_picture_file_size'] = array(
     '#type' => 'textfield',
     '#title' => t('Picture maximum file size'),
-    '#default_value' => variable_get('user_picture_file_size', '30'),
+    '#default_value' => variable_get('user_picture_file_size', '800'),
     '#size' => 15,
     '#maxlength' => 10,
     '#description' => t('Maximum file size for pictures, in kB.'),
diff --git modules/user/user.module modules/user/user.module
index 0547807..b6a30cd 100644
--- modules/user/user.module
+++ modules/user/user.module
@@ -625,8 +625,8 @@ function user_validate_picture(&$form, &$form_state) {
   // If required, validate the uploaded picture.
   $validators = array(
     'file_validate_is_image' => array(),
-    'file_validate_image_resolution' => array(variable_get('user_picture_dimensions', '85x85')),
-    'file_validate_size' => array(variable_get('user_picture_file_size', '30') * 1024),
+    'file_validate_image_resolution' => array(variable_get('user_picture_dimensions', '1024x1024')),
+    'file_validate_size' => array(variable_get('user_picture_file_size', '800') * 1024),
   );
 
   // Save the file as a temporary file.
@@ -1186,7 +1186,12 @@ function template_preprocess_user_picture(&$variables) {
     }
     if (isset($filepath)) {
       $alt = t("@user's picture", array('@user' => $account->name ? $account->name : variable_get('anonymous', t('Anonymous'))));
-      $variables['picture'] = theme('image', $filepath, $alt, $alt, '', FALSE);
+      if ($preset = variable_get('user_picture_preset', '')) {
+        $variables['picture'] = theme('image_preset', $preset, $filepath, $alt, $alt, NULL, FALSE);
+      }
+      else {
+        $variables['picture'] = theme('image', $filepath, $alt, $alt, NULL, FALSE);
+      }
       if (!empty($account->uid) && user_access('access user profiles')) {
         $attributes = array('attributes' => array('title' => t('View user profile.')), 'html' => TRUE);
         $variables['picture'] = l($variables['picture'], "user/$account->uid", $attributes);
@@ -1895,7 +1900,7 @@ function user_edit_form(&$form_state, $uid, $edit, $register = FALSE) {
       '#type' => 'file',
       '#title' => t('Upload picture'),
       '#size' => 48,
-      '#description' => t('Your virtual face or picture. Maximum dimensions are %dimensions pixels and the maximum size is %size kB.', array('%dimensions' => variable_get('user_picture_dimensions', '85x85'), '%size' => variable_get('user_picture_file_size', '30'))) . ' ' . variable_get('user_picture_guidelines', ''),
+      '#description' => t('Your virtual face or picture. Maximum dimensions are %dimensions pixels and the maximum size is %size kB.', array('%dimensions' => variable_get('user_picture_dimensions', '1024x1024'), '%size' => variable_get('user_picture_file_size', '800'))) . ' ' . variable_get('user_picture_guidelines', ''),
     );
     $form['#validate'][] = 'user_validate_picture';
   }
@@ -1996,6 +2001,9 @@ function _user_cancel($edit, $account, $method) {
       db_delete('authmap')
         ->condition('uid', $account->uid)
         ->execute();
+      if ($account->picture) {
+        file_delete($account->picture);
+      }
       drupal_set_message(t('%name has been deleted.', array('%name' => $account->name)));
       watchdog('user', 'Deleted user: %name %email.', array('%name' => $account->name, '%email' => '<' . $account->mail . '>'), WATCHDOG_NOTICE);
       break;
diff --git modules/user/user.test modules/user/user.test
index e0584ec..c06c9cc 100644
--- modules/user/user.test
+++ modules/user/user.test
@@ -555,8 +555,14 @@ class UserPictureTestCase extends DrupalWebTestCase {
         // user's profile page.
         $text = t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $test_dim));
         $this->assertRaw($text, t('Image was resized.'));
+        if ($preset = variable_get('user_picture_preset', '')) {
+          $url_path = image_preset_path($preset, $pic_path, FALSE);
+        }
+        else {
+          $url_path = $pic_path;
+        }
         $alt = t("@user's picture", array('@user' => $this->user->name));
-        $this->assertRaw(theme('image', $pic_path, $alt, $alt, '', FALSE), t("Image is displayed in user's edit page"));
+        $this->assertRaw(theme('image', $url_path, $alt, $alt, NULL, FALSE), t("Image is displayed in user's edit page"));
 
         // Check if file is located in proper directory.
         $this->assertTrue(is_file($pic_path), t("File is located in proper directory"));
@@ -689,8 +695,14 @@ class UserPictureTestCase extends DrupalWebTestCase {
       $pic_path = $this->saveUserPicture($image);
 
       // Check if image is displayed in user's profile page.
+      if ($preset = variable_get('user_picture_preset', '')) {
+        $url_path = image_preset_path($preset, $pic_path, FALSE);
+      }
+      else {
+        $url_path = $pic_path;
+      }
       $this->drupalGet('user');
-      $this->assertRaw($pic_path, t("Image is displayed in user's profile page"));
+      $this->assertRaw($url_path, t("Image is displayed in user's profile page"));
 
       // Check if file is located in proper directory.
       $this->assertTrue(is_file($pic_path), t('File is located in proper directory'));
diff --git profiles/default/default.profile profiles/default/default.profile
index e00b361..fa19fe9 100644
--- profiles/default/default.profile
+++ profiles/default/default.profile
@@ -186,6 +186,25 @@ function default_profile_tasks(&$task, $url) {
   // Don't display date and author information for page nodes by default.
   variable_set('node_submitted_page', FALSE);
 
+  // Create an image preset.
+  $preset = array('name' => 'thumbnail_square');
+  $preset = image_preset_save($preset);
+  $action = array(
+    'ipid' => $preset['ipid'],
+    'action' => 'system_image_scale_and_crop',
+    'data' => array('width' => '85', 'height' => '85'),
+  );
+  image_action_save($action);
+
+  // Enable user picture support and set the default to a square thumbnail option.
+  variable_set('user_pictures', '1');
+  variable_set('user_picture_preset', 'thumbnail_square');
+
+  $theme_settings = theme_get_settings();
+  $theme_settings['toggle_node_user_picture'] = '1';
+  $theme_settings['toggle_comment_user_picture'] = '1';
+  variable_set('theme_settings', $theme_settings);
+
   // Create a default vocabulary named "Tags", enabled for the 'article' content type.
   $description = st('Use tags to group articles on similar topics into categories.');
   $help = st('Enter a comma-separated list of words.');
