Commit 6dc5b544 authored by Dries's avatar Dries

- Patch #491456 by quicksketch, drewish, et al: image effects and actions.

parent 065fa605
......@@ -85,9 +85,7 @@
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);
......@@ -1500,6 +1498,23 @@ function file_directory_path() {
return variable_get('file_directory_path', conf_path() . '/files');
}
/**
* 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.
*
......
<?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".
*/
<?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;
}
; $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
<?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;
}
This diff is collapsed.
<?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'));