summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDries Buytaert <dries@buytaert.net>2009-07-12 08:36:35 +0000
committerDries Buytaert <dries@buytaert.net>2009-07-12 08:36:35 +0000
commit6dc5b5449d6c52b433e6b712786c2e49778a363f (patch)
tree66e4926fa3dcc0a33f46f4145ca5b72bf7458943
parent065fa60551edddc27a30d04cecb58b30b7ac8e3c (diff)
downloadbrdo-6dc5b5449d6c52b433e6b712786c2e49778a363f.tar.gz
brdo-6dc5b5449d6c52b433e6b712786c2e49778a363f.tar.bz2
- Patch #491456 by quicksketch, drewish, et al: image effects and actions.
-rw-r--r--includes/file.inc21
-rw-r--r--modules/image/image.api.php97
-rw-r--r--modules/image/image.effects.inc230
-rw-r--r--modules/image/image.info10
-rw-r--r--modules/image/image.install110
-rw-r--r--modules/image/image.module720
-rw-r--r--modules/image/image.test218
-rw-r--r--modules/simpletest/tests/image.test23
-rw-r--r--modules/user/user.admin.inc10
-rw-r--r--modules/user/user.module26
-rw-r--r--modules/user/user.test3
-rw-r--r--profiles/default/default.profile23
12 files changed, 1475 insertions, 16 deletions
diff --git a/includes/file.inc b/includes/file.inc
index 73d513703..08bbbfdd5 100644
--- a/includes/file.inc
+++ b/includes/file.inc
@@ -85,9 +85,7 @@ define('FILE_STATUS_PERMANENT', 1);
function file_create_url($path) {
// Strip file_directory_path from $path. We only include relative paths in
// URLs.
- if (strpos($path, file_directory_path() . '/') === 0) {
- $path = trim(substr($path, strlen(file_directory_path())), '\\/');
- }
+ $path = file_directory_strip($path);
switch (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC)) {
case FILE_DOWNLOADS_PUBLIC:
return $GLOBALS['base_url'] . '/' . file_directory_path() . '/' . str_replace('\\', '/', $path);
@@ -1501,6 +1499,23 @@ function file_directory_path() {
}
/**
+ * Remove a possible leading file directory path from the given path.
+ *
+ * @param $path
+ * Path to a file that may be in Drupal's files directory.
+ * @return
+ * String with Drupal's files directory removed from it.
+ */
+function file_directory_strip($path) {
+ // Strip file_directory_path from $path. We only include relative paths in
+ // URLs.
+ if (strpos($path, file_directory_path() . '/') === 0) {
+ $path = trim(substr($path, strlen(file_directory_path())), '\\/');
+ }
+ return $path;
+}
+
+/**
* Determine the maximum file upload size by querying the PHP settings.
*
* @return
diff --git a/modules/image/image.api.php b/modules/image/image.api.php
new file mode 100644
index 000000000..d285f3864
--- /dev/null
+++ b/modules/image/image.api.php
@@ -0,0 +1,97 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Hooks related to image styles and effects.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Define information about image effects provided by a module.
+ *
+ * This hook enables modules to define image manipulation effects for use with
+ * an image style.
+ *
+ * @return
+ * An array of image effects. This array is keyed on the machine-readable
+ * effect name. Each effect is defined as an associative array containing the
+ * following items:
+ * - "name": The human-readable name of the effect.
+ * - "effect callback": The function to call to perform this effect.
+ * - "help": (optional) A brief description of the effect that will be shown
+ * when adding or configuring this effect.
+ */
+function hook_image_effect_info() {
+ $effects = array();
+
+ $effects['mymodule_resize'] = array(
+ 'name' => t('Resize'),
+ 'help' => t('Resize an image to an exact set of dimensions, ignoring aspect ratio.'),
+ 'effect callback' => 'mymodule_resize_image',
+ );
+
+ return $effects;
+}
+
+/**
+ * Respond to image style updating.
+ *
+ * This hook enables modules to update settings that might be affected by
+ * changes to an image. For example, updating a module specific variable to
+ * reflect a change in the image style's name.
+ *
+ * @param $style
+ * The image style array that is being updated.
+ */
+function hook_image_style_save($style) {
+ // If a module defines an image style and that style is renamed by the user
+ // the module should update any references to that style.
+ if (isset($style['old_name']) && $style['old_name'] == variable_get('mymodule_image_style', '')) {
+ variable_set('mymodule_image_style', $style['name']);
+ }
+}
+
+/**
+ * Respond to image style deletion.
+ *
+ * This hook enables modules to update settings when a image style is being
+ * deleted. If a style is deleted, a replacement name may be specified in
+ * $style['name'] and the style being deleted will be specified in
+ * $style['old_name'].
+ *
+ * @param $style
+ * The image style array that being deleted.
+ */
+function hook_image_style_delete($style) {
+ // Administrators can choose an optional replacement style when deleting.
+ // Update the modules style variable accordingly.
+ if (isset($style['old_name']) && $style['old_name'] == variable_get('mymodule_image_style', '')) {
+ variable_set('mymodule_image_style', $style['name']);
+ }
+}
+
+/**
+ * Respond to image style flushing.
+ *
+ * This hook enables modules to take effect when a style is being flushed (all
+ * images are being deleted from the server and regenerated). Any
+ * module-specific caches that contain information related to the style should
+ * be cleared using this hook. This hook is called whenever a style is updated,
+ * deleted, any effect associated with the style is update or deleted, or when
+ * the user selects the style flush option.
+ *
+ * @param $style
+ * The image style array that is being flushed.
+ */
+function hook_image_style_flush($style) {
+ // Empty cached data that contains information about the style.
+ cache_clear_all('*', 'cache_mymodule', TRUE);
+}
+ /**
+ * @} End of "addtogroup hooks".
+ */
diff --git a/modules/image/image.effects.inc b/modules/image/image.effects.inc
new file mode 100644
index 000000000..ae2200399
--- /dev/null
+++ b/modules/image/image.effects.inc
@@ -0,0 +1,230 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Functions needed to execute image effects provided by Image module.
+ */
+
+/**
+ * Implement hook_image_effect_info().
+ */
+function image_image_effect_info() {
+ $effects = array(
+ 'image_resize' => array(
+ 'name' => t('Resize'),
+ 'help' => t('Resizing will make images an exact set of dimensions. This may cause images to be stretched or shrunk disproportionately.'),
+ 'effect callback' => 'image_resize_effect',
+ ),
+ 'image_scale' => array(
+ 'name' => t('Scale'),
+ 'help' => t('Scaling will maintain the aspect-ratio of the original image. If only a single dimension is specified, the other dimension will be calculated.'),
+ 'effect callback' => 'image_scale_effect',
+ ),
+ 'image_scale_and_crop' => array(
+ 'name' => t('Scale and Crop'),
+ 'help' => t('Scale and crop will maintain the aspect-ratio of the original image, then crop the larger dimension. This is most useful for creating perfectly square thumbnails without stretching the image.'),
+ 'effect callback' => 'image_scale_and_crop_effect',
+ ),
+ 'image_crop' => array(
+ 'name' => t('Crop'),
+ 'help' => t('Cropping will remove portions of an image to make it the specified dimensions.'),
+ 'effect callback' => 'image_crop_effect',
+ ),
+ 'image_desaturate' => array(
+ 'name' => t('Desaturate'),
+ 'help' => t('Desaturate converts an image to grayscale.'),
+ 'effect callback' => 'image_desaturate_effect',
+ ),
+ 'image_rotate' => array(
+ 'name' => t('Rotate'),
+ 'help' => t('Rotating an image may cause the dimensions of an image to increase to fit the diagonal.'),
+ 'effect callback' => 'image_rotate_effect',
+ ),
+ );
+
+ return $effects;
+}
+
+/**
+ * Image effect callback; Resize an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the resize effect with the
+ * following items:
+ * - "width": An integer representing the desired width in pixels.
+ * - "height": An integer representing the desired height in pixels.
+ * @return
+ * TRUE on success. FALSE on failure to resize image.
+ * @see image_resize()
+ */
+function image_resize_effect(&$image, $data) {
+ if (!image_resize($image, $data['width'], $data['height'])) {
+ watchdog('image', 'Image resize failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * Image effect callback; Scale an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the scale effect with the
+ * following items:
+ * - "width": An integer representing the desired width in pixels.
+ * - "height": An integer representing the desired height in pixels.
+ * - "upscale": A Boolean indicating that the image should be upscalled if
+ * the dimensions are larger than the original image.
+ * @return
+ * TRUE on success. FALSE on failure to scale image.
+ * @see image_scale()
+ */
+function image_scale_effect(&$image, $data) {
+ // Set sane default values.
+ $data += array(
+ 'upscale' => FALSE,
+ );
+
+ // Set impossibly large values if the width and height aren't set.
+ $data['width'] = empty($data['width']) ? PHP_INT_MAX : $data['width'];
+ $data['height'] = empty($data['height']) ? PHP_INT_MAX : $data['height'];
+
+ if (!image_scale($image, $data['width'], $data['height'], $data['upscale'])) {
+ watchdog('image', 'Image scale failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * Image effect callback; Crop an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the crop effect with the
+ * following items:
+ * - "width": An integer representing the desired width in pixels.
+ * - "height": An integer representing the desired height in pixels.
+ * - "anchor": A string describing where the crop should originate in the form
+ * of "XOFFSET-YOFFSET". XOFFSET is either a number of pixels or
+ * "left", "center", "right" and YOFFSET is either a number of pixels or
+ * "top", "center", "bottom".
+ * @return
+ * TRUE on success. FALSE on failure to crop image.
+ * @see image_crop()
+ */
+function image_crop_effect(&$image, $data) {
+ // Set sane default values.
+ $data += array(
+ 'anchor' => 'center-center',
+ );
+
+ list($x, $y) = explode('-', $data['anchor']);
+ $x = image_filter_keyword($x, $image->info['width'], $data['width']);
+ $y = image_filter_keyword($y, $image->info['height'], $data['height']);
+ if (!image_crop($image, $x, $y, $data['width'], $data['height'])) {
+ watchdog('image', 'Image crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * Image effect callback; Scale and crop an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the scale and crop effect
+ * with the following items:
+ * - "width": An integer representing the desired width in pixels.
+ * - "height": An integer representing the desired height in pixels.
+ * @return
+ * TRUE on success. FALSE on failure to scale and crop image.
+ * @see image_scale_and_crop()
+ */
+function image_scale_and_crop_effect(&$image, $data) {
+ if (!image_scale_and_crop($image, $data['width'], $data['height'])) {
+ watchdog('image', 'Image scale and crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * Image effect callback; Desaturate (grayscale) an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the desaturate effect.
+ * @return
+ * TRUE on success. FALSE on failure to desaturate image.
+ * @see image_desaturate()
+ */
+function image_desaturate_effect(&$image, $data) {
+ if (!image_desaturate($image)) {
+ watchdog('image', 'Image desaturate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * Image effect callback; Rotate an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the rotate effect containing
+ * the following items:
+ * - "degrees": The number of (clockwise) degrees to rotate the image.
+ * - "random": A Boolean indicating that a random rotation angle should be
+ * used for this image. The angle specified in "degrees" is used as a
+ * positive and negative maximum.
+ * - "bgcolor": The background color to use for exposed areas of the image.
+ * Use web-style hex colors (#FFFFFF for white, #000000 for black). Leave
+ * blank for transparency on image types that support it.
+ * @return
+ * TRUE on success. FALSE on failure to rotate image.
+ * @see image_rotate().
+ */
+function image_rotate_effect(&$image, $data) {
+ // Set sane default values.
+ $data += array(
+ 'degrees' => 0,
+ 'bgcolor' => NULL,
+ 'random' => FALSE,
+ );
+
+ // Convert short #FFF syntax to full #FFFFFF syntax.
+ if (strlen($data['bgcolor']) == 4) {
+ $c = $data['bgcolor'];
+ $data['bgcolor'] = $c[0] . $c[1] . $c[1] . $c[2] . $c[2] . $c[3] . $c[3];
+ }
+
+ // Convert #FFFFFF syntax to hexadecimal colors.
+ if ($data['bgcolor'] != '') {
+ $data['bgcolor'] = hexdec(str_replace('#', '0x', $data['bgcolor']));
+ }
+ else {
+ $data['bgcolor'] = NULL;
+ }
+
+ 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', 'Image rotate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
diff --git a/modules/image/image.info b/modules/image/image.info
new file mode 100644
index 000000000..148a095b7
--- /dev/null
+++ b/modules/image/image.info
@@ -0,0 +1,10 @@
+; $Id$
+name = Image
+description = Provides image manipulation tools.
+package = Core
+version = VERSION
+core = 7.x
+files[] = image.module
+files[] = image.effects.inc
+files[] = image.install
+files[] = image.test
diff --git a/modules/image/image.install b/modules/image/image.install
new file mode 100644
index 000000000..b174b13ee
--- /dev/null
+++ b/modules/image/image.install
@@ -0,0 +1,110 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Install, update and uninstall functions for the image module.
+ */
+
+/**
+ * Implement hook_install().
+ */
+function image_install() {
+ drupal_install_schema('image');
+
+ // Create the styles directory and ensure it's writable.
+ $path = file_directory_path() . '/styles';
+ file_check_directory($path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+}
+
+/**
+ * Implement hook_uninstall().
+ */
+function image_uninstall() {
+ drupal_uninstall_schema('image');
+
+ // Remove the styles directory and generated images.
+ $path = file_directory_path() . '/styles';
+ file_unmanaged_delete_recursive($path);
+}
+
+/**
+ * Implement hook_schema().
+ */
+function image_schema() {
+ $schema = array();
+
+ $schema['cache_image'] = drupal_get_schema_unprocessed('system', 'cache');
+ $schema['cache_image']['description'] = 'Cache table used to store information about image manipulations that are in-progress.';
+
+ $schema['image_styles'] = array(
+ 'description' => 'Stores configuration options for image styles.',
+ 'fields' => array(
+ 'isid' => array(
+ 'description' => 'The primary identifier for an image style.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'name' => array(
+ 'description' => 'The style name.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ ),
+ 'primary key' => array('isid'),
+ 'indexes' => array(
+ 'name' => array('name'),
+ ),
+ );
+
+ $schema['image_effects'] = array(
+ 'description' => 'Stores configuration options for image effects.',
+ 'fields' => array(
+ 'ieid' => array(
+ 'description' => 'The primary identifier for an image effect.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'isid' => array(
+ 'description' => 'The {image_styles}.isid for an image style.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'weight' => array(
+ 'description' => 'The weight of the effect in the style.',
+ 'type' => 'int',
+ 'unsigned' => FALSE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'name' => array(
+ 'description' => 'The unique name of the effect to be executed.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'data' => array(
+ 'description' => 'The configuration data for the effect.',
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ ),
+ 'primary key' => array('ieid'),
+ 'indexes' => array(
+ 'isid' => array('isid'),
+ 'weight' => array('weight'),
+ ),
+ 'foreign keys' => array(
+ 'isid' => array('image_styles' => 'isid'),
+ ),
+ );
+
+ return $schema;
+}
diff --git a/modules/image/image.module b/modules/image/image.module
new file mode 100644
index 000000000..a979e6e5f
--- /dev/null
+++ b/modules/image/image.module
@@ -0,0 +1,720 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Exposes global functionality for creating image styles.
+ */
+
+/**
+ * Implement hook_menu().
+ */
+function image_menu() {
+ $items = array();
+
+ $items['image/generate/%image_style'] = array(
+ 'title' => 'Generate image style',
+ 'page callback' => 'image_style_generate',
+ 'page arguments' => array(2),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+
+ return $items;
+}
+
+/**
+ * Implement hook_theme().
+ */
+function image_theme() {
+ return array(
+ 'image_style' => array(
+ 'arguments' => array('style' => NULL, 'path' => NULL, 'alt' => '', 'title' => '', 'attributes' => NULL, 'getsize' => TRUE),
+ ),
+ );
+}
+
+/**
+ * Implement hook_flush_caches().
+ */
+function image_flush_caches() {
+ return array('cache_image');
+}
+
+/**
+ * Implement hook_file_download().
+ *
+ * Control the access to files underneath the styles directory.
+ */
+function image_file_download($filepath) {
+ if (strpos($filepath, 'styles/') === 0) {
+ $args = explode('/', $filepath);
+ // Discard the first part of the path (styles).
+ array_shift($args);
+ // Get the style name from the second part.
+ $style_name = array_shift($args);
+ // Then the remaining parts are the path to the image.
+ $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 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;
+ }
+}
+
+/**
+ * Implement hook_file_move().
+ */
+function image_file_move($file, $source) {
+ // Delete any image derivatives at the original image path.
+ image_path_flush($file->filepath);
+}
+
+/**
+ * Implement hook_file_delete().
+ */
+function image_file_delete($file) {
+ // Delete any image derivatives of this image.
+ image_path_flush($file->filepath);
+}
+
+/**
+ * Clear cached versions of a specific file in all styles.
+ *
+ * @param $path
+ * The Drupal file path to the original image.
+ */
+function image_path_flush($path) {
+ $path = file_directory_strip($path);
+ $styles = image_styles();
+ foreach ($styles as $style) {
+ if ($path = file_create_path('styles/' . $style['name'] . '/' . $path)) {
+ file_unmanaged_delete($path);
+ }
+ }
+}
+
+/**
+ * Get an array of all styles and their settings.
+ *
+ * @return
+ * An array of styles keyed by the image style ID (isid).
+ * @see image_style_load()
+ */
+function image_styles() {
+ $styles = &drupal_static(__FUNCTION__);
+
+ // Grab from cache or build the array.
+ if (!isset($styles)) {
+ if ($cache = cache_get('image_styles', 'cache')) {
+ $styles = $cache->data;
+ }
+ else {
+ $styles = array();
+ $result = db_select('image_styles', NULL, array('fetch' => PDO::FETCH_ASSOC))
+ ->fields('image_styles')
+ ->orderBy('name')
+ ->execute();
+ foreach ($result as $style) {
+ $styles[$style['name']] = $style;
+ $styles[$style['name']]['effects'] = image_style_effects($style);
+ }
+
+ cache_set('image_styles', $styles);
+ }
+ }
+
+ return $styles;
+}
+
+/**
+ * Load a style by style name or ID. May be used as a loader for menu items.
+ *
+ * @param $name
+ * The name of the style.
+ * @param $isid
+ * Optional. The numeric id of a style if the name is not known.
+ * @return
+ * An image style array containing the following keys:
+ * - "isid": The unique image style ID.
+ * - "name": The unique image style name.
+ * - "effects": An array of effects within this style.
+ * If the style name or ID is not valid, an empty array is returned.
+ * @see image_effect_load()
+ */
+function image_style_load($name = NULL, $isid = NULL) {
+ $styles = image_styles();
+
+ // If retrieving by name.
+ if (isset($name) && isset($styles[$name])) {
+ return $styles[$name];
+ }
+
+ // If retrieving by image style id.
+ if (isset($isid)) {
+ foreach ($styles as $name => $style) {
+ if ($style['isid'] == $isid) {
+ return $style;
+ }
+ }
+ }
+
+ // Otherwise the style was not found.
+ return FALSE;
+}
+
+/**
+ * Save an image style.
+ *
+ * @param style
+ * An image style array.
+ * @return
+ * A style array. In the case of a new style, 'isid' will be populated.
+ */
+function image_style_save($style) {
+ if (isset($style['isid']) && is_numeric($style['isid'])) {
+ // Load the existing style to make sure we account for renamed styles.
+ $old_style = image_style_load(NULL, $style['isid']);
+ image_style_flush($old_style);
+ drupal_write_record('image_styles', $style, 'isid');
+ if ($old_style['name'] != $style['name']) {
+ $style['old_name'] = $old_style['name'];
+ }
+ }
+ else {
+ drupal_write_record('image_styles', $style);
+ $style['is_new'] = TRUE;
+ }
+
+ // Let other modules update as necessary on save.
+ module_invoke_all('image_style_save', $style);
+
+ // Clear all caches and flush.
+ image_style_flush($style);
+
+ return $style;
+}
+
+/**
+ * Delete an image style.
+ *
+ * @param $style
+ * An image style array.
+ * @param $replacement_style_name
+ * (optional) When deleting a style, specify a replacement style name so
+ * that existing settings (if any) may be converted to a new style.
+ * @return
+ * TRUE on success.
+ */
+function image_style_delete($style, $replacement_style_name = '') {
+ image_style_flush($style);
+
+ db_delete('image_effects')->condition('isid', $style['isid'])->execute();
+ db_delete('image_styles')->condition('isid', $style['isid'])->execute();
+
+ // Let other modules update as necessary on save.
+ $style['old_name'] = $style['name'];
+ $style['name'] = $replacement_style_name;
+ module_invoke_all('image_style_delete', $style);
+
+ return TRUE;
+}
+
+/**
+ * Load all the effects for an image style.
+ *
+ * @param $style
+ * An image style array.
+ * @return
+ * An array of effects associated with specified style in the format
+ * array('isid' => array()), or an empty array if the specified style has
+ * no effects.
+ */
+function image_style_effects($style) {
+ $effects = image_effects();
+ $style_effects = array();
+ foreach ($effects as $effect) {
+ if ($style['isid'] == $effect['isid']) {
+ $style_effects[$effect['ieid']] = $effect;
+ }
+ }
+
+ return $style_effects;
+}
+
+/**
+ * Get an array of image styles 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 styles both key and value are set to style name.
+ */
+function image_style_options($include_empty = TRUE) {
+ $styles = image_styles();
+ $options = array();
+ if ($include_empty && !empty($styles)) {
+ $options[''] = t('<none>');
+ }
+ $options = array_merge($options, drupal_map_assoc(array_keys($styles)));
+ if (empty($options)) {
+ $options[''] = t('No defined styles');
+ }
+ return $options;
+}
+
+/**
+ * Menu callback; Given a style and image path, generate a derivative.
+ *
+ * This menu callback is always served after checking a token to prevent
+ * generation of unnecessary images. After generating an image transfer it to
+ * the requesting agent via file_transfer().
+ */
+function image_style_generate() {
+ $args = func_get_args();
+ $style = array_shift($args);
+ $style_name = $style['name'];
+ $path = implode('/', $args);
+
+ $source = file_create_path($path);
+ $path_md5 = md5($path);
+ $destination = image_style_path($style['name'], $path);
+
+ // Check that it's a defined style and that access was granted by
+ // image_style_generate_url().
+ if (!$style || !cache_get('access:' . $style_name . ':' . $path_md5, 'cache_image')) {
+ drupal_access_denied();
+ exit();
+ }
+
+ // Don't start generating the image if it is already in progress.
+ $cid = 'generate:' . $style_name . ':' . $path_md5;
+ if (cache_get($cid, 'cache_image')) {
+ print t('Image generation in progress, please try again shortly.');
+ exit();
+ }
+
+ // If the image has already been generated then send it.
+ if ($image = image_load($destination)) {
+ file_transfer($image->source, array('Content-type: ' . $image->info['mime_type'], 'Content-length: ' . $image->info['file_size']));
+ }
+
+ // Set a cache entry designating this image as being in-process.
+ cache_set($cid, $destination, 'cache_image');
+
+ // Try to generate the image.
+ if (image_style_create_derivative($style, $source, $destination)) {
+ $image = image_load($destination);
+ cache_clear_all($cid, 'cache_image');
+ file_transfer($image->source, array('Content-type: ' . $image->info['mime_type'], 'Content-length: ' . $image->info['file_size']));
+ }
+ else {
+ cache_clear_all($cid, 'cache_image');
+ watchdog('image', 'Unable to generate the derived image located at %path.', $destination);
+ print t('Error generating image.');
+ exit();
+ }
+}
+
+/**
+ * Create a new image based on an image style.
+ *
+ * @param $style
+ * An image style 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_style_create_derivative($style, $source, $destination) {
+ // Get the folder for the final location of this style.
+ $directory = dirname($destination);
+
+ // Build the destination folder tree if it doesn't already exist.
+ if (!file_check_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
+ watchdog('image', 'Failed to create style directory: %directory', array('%directory' => $directory), WATCHDOG_ERROR);
+ return FALSE;
+ }
+
+ if (!$image = image_load($source)) {
+ return FALSE;
+ }
+
+ foreach ($style['effects'] as $effect) {
+ image_effect_apply($image, $effect);
+ }
+
+ 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;
+}
+
+/**
+ * Flush cached media for a style.
+ *
+ * @param $style
+ * An image style array.
+ */
+function image_style_flush($style) {
+ $style_directory = realpath(file_directory_path() . '/styles/' . $style['name']);
+ if (is_dir($style_directory)) {
+ file_unmanaged_delete_recursive($style_directory);
+ }
+
+ // Let other modules update as necessary on flush.
+ module_invoke_all('image_style_flush', $style);
+
+ // Clear image style and effect caches.
+ cache_clear_all('image_styles', 'cache');
+ cache_clear_all('image_effects', 'cache');
+ drupal_static_reset('image_styles');
+ drupal_static_reset('image_effects');
+
+ // Clear page caches when flushing.
+ if (module_exists('block')) {
+ cache_clear_all('*', 'cache_block', TRUE);
+ }
+ cache_clear_all('*', 'cache_page', TRUE);
+}
+
+/**
+ * Return the complete URL to an image when using a style.
+ *
+ * If the image has already been created then its location will be returned. If
+ * it does not then image_style_generate_url() will be called.
+ *
+ * @param $style_name
+ * The name of the style to be used with this image.
+ * @param $path
+ * The path to the image.
+ * @return
+ * The absolute URL where a style image can be downloaded, suitable for use
+ * in an <img> tag. If the site is using the default method for generating
+ * images, the image may not yet exist and will only be created when a
+ * visitor's browser requests the file.
+ * @see image_style_generate_url()
+ * @see image_style_path()
+ */
+function image_style_url($style_name, $path) {
+ $style_path = image_style_path($style_name, $path);
+ if (file_exists($style_path)) {
+ return file_create_url($style_path);
+ }
+ return image_style_generate_url($style_name, $path);
+}
+
+/**
+ * Return the URL for an image derivative given a style and image path.
+ *
+ * This function is the default image generation method. It returns a URL for
+ * an image that can be used in an <img> tag. When the browser requests the
+ * image at image/generate/[style_name]/[path] the image is generated if it does
+ * not already exist and then served to the browser. This allows each image to
+ * have its own PHP instance (and memory limit) for generation of the new image.
+ *
+ * @param $style_name
+ * The name of the style to be used with this image.
+ * @param $path
+ * The path to the image.
+ * @return
+ * The absolute URL where a style image can be downloaded, suitable for use
+ * in an <img> tag. Requesting the URL will cause the image to be created.
+ * @see image_style_generate()
+ * @see image_style_url()
+ */
+function image_style_generate_url($style_name, $path) {
+ $destination = image_style_path($style_name, $path);
+
+ // If the image already exists use that rather than regenerating it.
+ if (file_exists($destination)) {
+ return image_style_url($style_name, $path);
+ }
+
+ // Disable page cache for this request. This prevents anonymous users from
+ // needlessly hitting the image generation URL when the image already exists.
+ $GLOBALS['conf']['cache'] = CACHE_DISABLED;
+
+ // Set a cache entry to grant access to this style/image path. This will be
+ // checked by image_style_generate().
+ cache_set('access:' . $style_name . ':' . md5($path), 1, 'cache_image', time() + 600);
+
+ // Generate a callback path for the image.
+ $url = url('image/generate/' . $style_name . '/' . $path, array('absolute' => TRUE));
+ return $url;
+}
+
+/**
+ * Return a relative path to an image when using a style.
+ *
+ * The path returned by this function may not exist. The default generation
+ * method only creates images when they are requested by a user's browser.
+ *
+ * @param $style_name
+ * The name of the style to be used with this image.
+ * @param $path
+ * The path to the image.
+ * @return
+ * The path to an image style image relative to Drupal's root.
+ * @see image_style_url()
+ */
+function image_style_path($style_name, $path) {
+ return file_directory_path() . '/styles/' . $style_name . '/' . file_directory_strip($path);
+}
+
+/**
+ * Pull in effects exposed by other modules using hook_image_effect_info().
+ *
+ * @return
+ * An array of effects to be used when transforming images.
+ * @see hook_image_effect_info()
+ * @see image_effect_definition_load()
+ */
+function image_effect_definitions() {
+ $effects = &drupal_static(__FUNCTION__);
+
+ if (!isset($effects)) {
+ if ($cache = cache_get('image_effects') && !empty($cache->data)) {
+ $effects = $cache->data;
+ }
+ else {
+ $effects = array();
+ foreach (module_implements('image_effect_info') as $module) {
+ foreach (module_invoke($module, 'image_effect_info') as $name => $effect) {
+ // Ensure the current toolkit supports the effect.
+ $effect['module'] = $module;
+ $effect['name'] = $name;
+ $effect['data'] = isset($effect['data']) ? $effect['data'] : array();
+ $effects[$name] = $effect;
+ };
+ }
+ uasort($effects, '_image_effect_definitions_sort');
+ cache_set('image_effects', $effects);
+ }
+ }
+
+ return $effects;
+}
+
+/**
+ * Load the definition for an effect.
+ *
+ * The effect definition is a set of core properties for an effect, not
+ * containing any user-settings. The definition defines various functions to
+ * call when configuring or executing an effect. This loader is mostly for
+ * internal use within image.module. Use image_effect_load() or
+ * image_style_load() to get effects that contain configuration.
+ *
+ * @param $effect
+ * The name of the effect definition to load.
+ * @return
+ * An array containing the image effect definition with the following keys:
+ * - "effect": The unique name for the effect being performed. Usually prefixed
+ * with the name of the module providing the effect.
+ * - "module": The module providing the effect.
+ * - "help": A description of the effect.
+ * - "function": The name of the function that will execute the effect.
+ * - "form": i'm (optional) The name of a function to configure the effect.
+ * - "summary": (optional) The name of a theme function that will display a
+ * one-line summary of the effect. Does not include the "theme_" prefix.
+ */
+function image_effect_definition_load($effect) {
+ $definitions = image_effect_definitions();
+ return isset($definitions[$effect]) ? $definitions[$effect] : FALSE;
+}
+
+/**
+ * Load all image effects from the database.
+ *
+ * @return
+ * An array of all image effects.
+ * @see image_effect_load()
+ */
+function image_effects() {
+ $effects = &drupal_static(__FUNCTION__);
+
+ if (!isset($effects)) {
+ $effects = array();
+
+ // Add database image effects.
+ $result = db_select('image_effects', NULL, array('fetch' => PDO::FETCH_ASSOC))
+ ->fields('image_effects')
+ ->orderBy('image_effects.weight', 'ASC')
+ ->execute();
+ foreach ($result as $effect) {
+ $effect['data'] = unserialize($effect['data']);
+ $definition = image_effect_definition_load($effect['name']);
+ // Do not load effects whose definition cannot be found.
+ if ($definition) {
+ $effect = array_merge($definition, $effect);
+ $effects[$effect['ieid']] = $effect;
+ }
+ }
+ }
+
+ return $effects;
+}
+
+/**
+ * Load a single image effect.
+ *
+ * @param $ieid
+ * The image effect ID.
+ * @return
+ * An image effect array, consisting of the following keys:
+ * - "ieid": The unique image effect ID.
+ * - "isid": The unique image style ID that contains this effect.
+ * - "weight": The weight of this effect within the image style.
+ * - "effect": The name of the effect definition that powers this effect.
+ * - "data": An associative array of configuration options for this effect.
+ * Besides these keys, the entirety of the image definition is merged into
+ * the image effect array. Returns FALSE if the specified effect cannot be
+ * found.
+ * @see image_style_load()
+ * @see image_effect_definition_load()
+ */
+function image_effect_load($ieid) {
+ $effects = image_effects();
+ return isset($effects[$ieid]) ? $effects[$ieid] : FALSE;
+}
+
+/**
+ * Save an image effect.
+ *
+ * @param $effect
+ * An image effect array.
+ * @return
+ * An image effect array. In the case of a new effect 'ieid' will be set.
+ */
+function image_effect_save($effect) {
+ if (!empty($effect['ieid'])) {
+ drupal_write_record('image_effects', $effect, 'ieid');
+ }
+ else {
+ drupal_write_record('image_effects', $effect);
+ }
+ $style = image_style_load(NULL, $effect['isid']);
+ image_style_flush($style);
+ return $effect;
+}
+
+/**
+ * Delete an image effect.
+ *
+ * @param $effect
+ * An image effect array.
+ */
+function image_effect_delete($effect) {
+ db_delete('image_effects')->condition('ieid', $effect['ieid'])->execute();
+ $style = image_style_load(NULL, $effect['isid']);
+ image_style_flush($style);
+}
+
+/**
+ * Given an image object and effect, perform the effect on the file.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $effect
+ * An image effect array.
+ * @return
+ * TRUE on success. FALSE if unable to perform effect on image.
+ */
+function image_effect_apply(&$image, $effect) {
+ if (drupal_function_exists($effect['effect callback'])) {
+ return call_user_func($effect['effect callback'], $image, $effect['data']);
+ }
+ return FALSE;
+}
+
+/**
+ * Return a themed image using a specific image style.
+ *
+ * @param $style_name
+ * The name of the style 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.
+ * @ingroup themeable
+ */
+function theme_image_style($style_name, $path, $alt = '', $title = '', $attributes = NULL, $getsize = TRUE) {
+ // theme_image() can only honor the $getsize parameter with local file paths.
+ // The derivative image is not created until it has been requested so the file
+ // may not yet exist, in this case we just fallback to the URL.
+ $style_path = image_style_path($style_name, $path);
+ if (!file_exists($style_path)) {
+ $style_path = image_style_url($style_name, $path);
+ }
+ return theme('image', $style_path, $alt, $title, $attributes, $getsize);
+}
+
+/**
+ * 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 a pixel offset.
+ *
+ * @param $value
+ * @param $current_pixels
+ * @param $new_pixels
+ */
+function image_filter_keyword($value, $current_pixels, $new_pixels) {
+ switch ($value) {
+ case 'top':
+ case 'left':
+ return 0;
+
+ case 'bottom':
+ case 'right':
+ return $current_pixels - $new_pixels;
+
+ case 'center':
+ return $current_pixels / 2 - $new_pixels / 2;
+ }
+ return $value;
+}
+
+/**
+ * Internal function for sorting image effect definitions through uasort().
+ *
+ * @see image_effect_definitions()
+ */
+function _image_effect_definitions_sort($a, $b) {
+ return strcasecmp($a['name'], $b['name']);
+}
diff --git a/modules/image/image.test b/modules/image/image.test
new file mode 100644
index 000000000..ec8525518
--- /dev/null
+++ b/modules/image/image.test
@@ -0,0 +1,218 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Image module tests.
+ */
+
+
+/**
+ * TODO: Test the following functions.
+ *
+ * image.effects.inc:
+ * image_style_generate()
+ * image_style_create_derivative()
+ *
+ * image.module:
+ * image_style_load()
+ * image_style_save()
+ * image_style_delete()
+ * image_style_options()
+ * image_style_flush()
+ * image_effect_definition_load()
+ * image_effect_load()
+ * image_effect_save()
+ * image_effect_delete()
+ * image_filter_keyword()
+ */
+
+/**
+ * Tests the functions for generating paths and URLs for image styles.
+ */
+class ImageStylesPathAndUrlUnitTest extends DrupalWebTestCase {
+ protected $style_name;
+ protected $image_with_generated;
+ protected $image_without_generated;
+
+ function getInfo() {
+ return array(
+ 'name' => t('Image styles path and URL functions'),
+ 'description' => t('Tests functions for generating paths and URLs to image styles.'),
+ 'group' => t('Image')
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+
+ $this->style_name = 'style_foo';
+
+ // Create the directories for the styles.
+ $status = file_check_directory($d = file_directory_path() .'/styles/' . $this->style_name, FILE_CREATE_DIRECTORY);
+ $this->assertNotIdentical(FALSE, $status, t('Created the directory for the generated images for the test style.' ));
+
+ // Make two copies of the file...
+ $file = reset($this->drupalGetTestFiles('image'));
+ $this->image_without_generated = file_unmanaged_copy($file->filepath, NULL, FILE_EXISTS_RENAME);
+ $this->assertNotIdentical(FALSE, $this->image_without_generated, t('Created the without generated image file.'));
+ $this->image_with_generated = file_unmanaged_copy($file->filepath, NULL, FILE_EXISTS_RENAME);
+ $this->assertNotIdentical(FALSE, $this->image_with_generated, t('Created the with generated image file.'));
+ // and create a "generated" file for the one.
+ $status = file_unmanaged_copy($file->filepath, image_style_path($this->style_name, $this->image_with_generated), FILE_EXISTS_REPLACE);
+ $this->assertNotIdentical(FALSE, $status, t('Created a file where the generated image should be.'));
+ }
+
+ /**
+ * Test image_style_path().
+ */
+ function testImageStylePath() {
+ $actual = image_style_path($this->style_name, $this->image_without_generated);
+ $expected = file_directory_path() . '/styles/' . $this->style_name . '/' . basename($this->image_without_generated);
+ $this->assertEqual($actual, $expected, t('Got the path for a file.'));
+ }
+
+ /**
+ * Test image_style_url().
+ */
+ function testImageStyleUrl() {
+ // Test it with no generated file.
+ $actual = image_style_url($this->style_name, $this->image_without_generated);
+ $expected = url('image/generate/' . $this->style_name . '/' . $this->image_without_generated, array('absolute' => TRUE));
+ $this->assertEqual($actual, $expected, t('Got the generate URL for a non-existent file.'));
+
+ // Now test it with a generated file.
+ $actual = image_style_url($this->style_name, $this->image_with_generated);
+ $expected = file_create_url(image_style_path($this->style_name, $this->image_with_generated));
+ $this->assertEqual($actual, $expected, t('Got the download URL for an existing file.'));
+ }
+
+ /**
+ * Test image_style_generate_url().
+ */
+ function testImageStyleGenerateUrl() {
+ // Test it with no generated file.
+ $actual = image_style_generate_url($this->style_name, $this->image_without_generated);
+ $expected = url('image/generate/' . $this->style_name . '/' . $this->image_without_generated, array('absolute' => TRUE));
+ $this->assertEqual($actual, $expected, t('Got the generate URL for a non-existent file.'));
+
+ // Now test it with a generated file.
+ $actual = image_style_generate_url($this->style_name, $this->image_with_generated);
+ $expected = file_create_url(image_style_path($this->style_name, $this->image_with_generated));
+ $this->assertEqual($actual, $expected, t('Got the download URL for an existing file.'));
+ }
+}
+
+/**
+ * Use the image_test.module's mock toolkit to ensure that the effects are
+ * properly passing parameters to the image toolkit.
+ */
+class ImageEffectsUnitTest extends ImageToolkitTestCase {
+ function getInfo() {
+ return array(
+ 'name' => t('Image effects'),
+ 'description' => t('Test that the image effects pass parameters to the toolkit correctly.'),
+ 'group' => t('Image')
+ );
+ }
+
+ function setUp() {
+ parent::setUp('image_test');
+ module_load_include('inc', 'image', 'image.effects');
+ }
+
+ /**
+ * Test the image_effects() and image_effect_definitions() functions.
+ */
+ function testEffects() {
+ $effects = image_effects();
+ $this->assertEqual(count($effects), 1, t("Found core's effect."));
+
+ $effect_definitions = image_effect_definitions();
+ $this->assertEqual(count($effect_definitions), 6, t("Found core's effects."));
+ }
+
+ /**
+ * Test the image_resize_effect() function.
+ */
+ function testResizeEffect() {
+ $this->assertTrue(image_resize_effect($this->image, array('width' => 1, 'height' => 2)), t('Function returned the expected value.'));
+ $this->assertToolkitOperationsCalled(array('resize'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual($calls['resize'][0][1], 1, t('Width was passed correctly'));
+ $this->assertEqual($calls['resize'][0][2], 2, t('Height was passed correctly'));
+ }
+
+ /**
+ * Test the image_scale_effect() function.
+ */
+ function testScaleEffect() {
+ // @todo: need to test upscaling.
+ $this->assertTrue(image_scale_effect($this->image, array('width' => 10, 'height' => 10)), t('Function returned the expected value.'));
+ $this->assertToolkitOperationsCalled(array('resize'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual($calls['resize'][0][1], 10, t('Width was passed correctly'));
+ $this->assertEqual($calls['resize'][0][2], 5, t('Height was based off aspect ratio and passed correctly'));
+ }
+
+ /**
+ * Test the image_crop_effect() function.
+ */
+ function testCropEffect() {
+ // @todo should test the keyword offsets.
+ $this->assertTrue(image_crop_effect($this->image, array('anchor' => 'top-1', 'width' => 3, 'height' => 4)), t('Function returned the expected value.'));
+ $this->assertToolkitOperationsCalled(array('crop'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual($calls['crop'][0][1], 0, t('X was passed correctly'));
+ $this->assertEqual($calls['crop'][0][2], 1, t('Y was passed correctly'));
+ $this->assertEqual($calls['crop'][0][3], 3, t('Width was passed correctly'));
+ $this->assertEqual($calls['crop'][0][4], 4, t('Height was passed correctly'));
+ }
+
+ /**
+ * Test the image_scale_and_crop_effect() function.
+ */
+ function testScaleAndCropEffect() {
+ $this->assertTrue(image_scale_and_crop_effect($this->image, array('width' => 5, 'height' => 10)), t('Function returned the expected value.'));
+ $this->assertToolkitOperationsCalled(array('resize', 'crop'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual($calls['crop'][0][1], 7.5, t('X was computed and passed correctly'));
+ $this->assertEqual($calls['crop'][0][2], 0, t('Y was computed and passed correctly'));
+ $this->assertEqual($calls['crop'][0][3], 5, t('Width was computed and passed correctly'));
+ $this->assertEqual($calls['crop'][0][4], 10, t('Height was computed and passed correctly'));
+ }
+
+ /**
+ * Test the image_desaturate_effect() function.
+ */
+ function testDesaturateEffect() {
+ $this->assertTrue(image_desaturate_effect($this->image, array()), t('Function returned the expected value.'));
+ $this->assertToolkitOperationsCalled(array('desaturate'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual(count($calls['desaturate'][0]), 1, t('Only the image was passed.'));
+ }
+
+ /**
+ * Test the image_rotate_effect() function.
+ */
+ function testRotateEffect() {
+ // @todo: need to test with 'random' => TRUE
+ $this->assertTrue(image_rotate_effect($this->image, array('degrees' => 90, 'bgcolor' => '#fff')), t('Function returned the expected value.'));
+ $this->assertToolkitOperationsCalled(array('rotate'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual($calls['rotate'][0][1], 90, t('Degrees were passed correctly'));
+ $this->assertEqual($calls['rotate'][0][2], 0xffffff, t('Background color was passed correctly'));
+ }
+}
diff --git a/modules/simpletest/tests/image.test b/modules/simpletest/tests/image.test
index d25d89582..bf0d717f2 100644
--- a/modules/simpletest/tests/image.test
+++ b/modules/simpletest/tests/image.test
@@ -14,14 +14,6 @@ class ImageToolkitTestCase extends DrupalWebTestCase {
protected $file;
protected $image;
- public static function getInfo() {
- return array(
- 'name' => t('Image toolkit tests'),
- 'description' => t('Check image tookit functions.'),
- 'group' => t('Image API'),
- );
- }
-
function setUp() {
parent::setUp('image_test');
@@ -72,6 +64,19 @@ class ImageToolkitTestCase extends DrupalWebTestCase {
$this->assertTrue(TRUE, t('No unexpected operations were called.'));
}
}
+}
+
+/**
+ * Test that the functions in image.inc correctly pass data to the toolkit.
+ */
+class ImageToolkitUnitTest extends ImageToolkitTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => t('Image toolkit tests'),
+ 'description' => t('Check image toolkit functions.'),
+ 'group' => t('Image'),
+ );
+ }
/**
* Check that hook_image_toolkits() is called and only available toolkits are
@@ -207,7 +212,7 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase {
return array(
'name' => t('Image GD manipulation tests'),
'description' => t('Check that core image manipulations work properly: scale, resize, rotate, crop, scale and crop, and desaturate.'),
- 'group' => t('Image API'),
+ 'group' => t('Image'),
);
}
diff --git a/modules/user/user.admin.inc b/modules/user/user.admin.inc
index fede3de58..2be697195 100644
--- a/modules/user/user.admin.inc
+++ b/modules/user/user.admin.inc
@@ -360,6 +360,14 @@ function user_admin_settings() {
'#maxlength' => 255,
'#description' => t('URL of picture to display for users with no custom picture selected. Leave blank for none.'),
);
+ if (module_exists('image')) {
+ $form['personalization']['pictures']['settings']['user_picture_style'] = array(
+ '#type' => 'select',
+ '#title' => t('Picture style'),
+ '#options' => image_style_options(TRUE),
+ '#default_value' => variable_get('user_picture_style', ''),
+ );
+ }
$form['personalization']['pictures']['user_picture_dimensions'] = array(
'#type' => 'textfield',
'#title' => t('Picture maximum dimensions'),
@@ -456,7 +464,7 @@ function user_admin_settings() {
'#default_value' => _user_mail_text('register_no_approval_required_body'),
'#rows' => 15,
);
-
+
$form['email_password_reset'] = array(
'#type' => 'fieldset',
'#title' => t('Password recovery'),
diff --git a/modules/user/user.module b/modules/user/user.module
index 3072ae2ef..ac7f6cbff 100644
--- a/modules/user/user.module
+++ b/modules/user/user.module
@@ -1176,7 +1176,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 (module_exists('image') && $style = variable_get('user_picture_style', '')) {
+ $variables['picture'] = theme('image_style', $style, $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);
@@ -2653,6 +2658,25 @@ function user_node_load($nodes, $types) {
}
/**
+ * Implement hook_image_style_delete().
+ */
+function user_image_style_delete($style) {
+ // If a style is deleted, update the variables.
+ // Administrators choose a replacement style when deleting.
+ user_image_style_save($style);
+}
+
+/**
+ * Implement hook_image_style_save().
+ */
+function user_image_style_save($style) {
+ // If a style is renamed, update the variables that use it.
+ if (isset($style['old_name']) && $style['old_name'] == variable_get('user_picture_style', '')) {
+ variable_set('user_picture_style', $style['name']);
+ }
+}
+
+/**
* Implement hook_hook_info().
*/
function user_hook_info() {
diff --git a/modules/user/user.test b/modules/user/user.test
index 2683dfb32..ee77d0dcc 100644
--- a/modules/user/user.test
+++ b/modules/user/user.test
@@ -563,7 +563,8 @@ class UserPictureTestCase extends DrupalWebTestCase {
$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.'));
$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"));
+ $style = variable_get('user_picture_style', '');
+ $this->assertRaw(image_style_url($style, $pic_path), 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"));
diff --git a/profiles/default/default.profile b/profiles/default/default.profile
index 52aa21068..256e26062 100644
--- a/profiles/default/default.profile
+++ b/profiles/default/default.profile
@@ -8,7 +8,7 @@
* An array of modules to enable.
*/
function default_profile_modules() {
- return array('block', 'color', 'comment', 'help', 'menu', 'path', 'taxonomy', 'dblog', 'search', 'toolbar');
+ return array('block', 'color', 'comment', 'help', 'image', 'menu', 'path', 'taxonomy', 'dblog', 'search', 'toolbar');
}
/**
@@ -196,6 +196,27 @@ 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 style.
+ $style = array('name' => 'thumbnail');
+ $style = image_style_save($style);
+ $effect = array(
+ 'isid' => $style['isid'],
+ 'name' => 'image_scale_and_crop',
+ 'data' => array('width' => '85', 'height' => '85'),
+ );
+ image_effect_save($effect);
+
+ // Enable user picture support and set the default to a square thumbnail option.
+ variable_set('user_pictures', '1');
+ variable_set('user_picture_dimensions', '1024x1024');
+ variable_set('user_picture_file_size', '800');
+ variable_set('user_picture_style', 'thumbnail');
+
+ $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.');