Commit 2da6a0ed authored by webchick's avatar webchick
Browse files

#371374 by quicksketch, drewish, eojthebrave, and dopry: ImageCache UI in...

#371374 by quicksketch, drewish, eojthebrave, and dopry: ImageCache UI in core. Hell yeah! :D Resizable, scalable, croppable, grey-scalable, rotatey images for all.
parent 6199ae64
......@@ -103,6 +103,11 @@ Drupal 7.0, xxxx-xx-xx (development version)
uploading a site logo--that don't require the overhead of databases and
hooks, the current unmanaged copy, move and delete operations have been
preserved but renamed to file_unmanaged_*().
- Image handling:
* Improved image handling, including better support for add-on image
libraries.
* Added API and interface for creating advanced image thumbnails.
* Inclusion of additional effects such as rotate and desaturate.
- Added aliased multi-site support:
* Added support for mapping domain names to sites directories.
- Added RDF support:
......
/* $Id$ */
/**
* Image style configuration pages.
*/
div.image-style-new,
div.image-style-new div {
display: inline;
}
div.image-style-preview div.preview-image-wrapper {
float: left;
padding-bottom: 2em;
text-align: center;
top: 50%;
width: 48%;
}
div.image-style-preview div.preview-image {
margin: auto;
position: relative;
}
div.image-style-preview div.preview-image div.width {
border: 1px solid #666;
border-top: none;
height: 2px;
left: -1px;
bottom: -6px;
position: absolute;
}
div.image-style-preview div.preview-image div.width span {
position: relative;
top: 4px;
}
div.image-style-preview div.preview-image div.height {
border: 1px solid #666;
border-left: none;
position: absolute;
right: -6px;
top: -1px;
width: 2px;
}
div.image-style-preview div.preview-image div.height span {
height: 2em;
left: 10px;
margin-top: -1em;
position: absolute;
top: 50%;
}
/**
* Image anchor element.
*/
table.image-anchor {
width: auto;
}
table.image-anchor tr.even,
table.image-anchor tr.odd {
background: none;
}
table.image-anchor td {
border: 1px solid #CCC;
}
This diff is collapsed.
......@@ -21,18 +21,24 @@
* 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.
* - "label": The human-readable name of the effect.
* - "effect callback": The function to call to perform this image effect.
* - "help": (optional) A brief description of the effect that will be shown
* when adding or configuring this effect.
* when adding or configuring this image effect.
* - "form callback": (optional) The name of a function that will return a
* $form array providing a configuration form for this image effect.
* - "summary theme": (optional) The name of a theme function that will output
* a summary of this image effect's configuration.
*/
function hook_image_effect_info() {
$effects = array();
$effects['mymodule_resize'] = array(
'name' => t('Resize'),
'label' => t('Resize'),
'help' => t('Resize an image to an exact set of dimensions, ignoring aspect ratio.'),
'effect callback' => 'mymodule_resize_image',
'form callback' => 'mymodule_resize_form',
'summary theme' => 'mymodule_resize_summary',
);
return $effects;
......
......@@ -12,34 +12,44 @@
function image_image_effect_info() {
$effects = array(
'image_resize' => array(
'name' => t('Resize'),
'label' => 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',
'form callback' => 'image_resize_form',
'summary theme' => 'image_resize_summary',
),
'image_scale' => array(
'name' => t('Scale'),
'label' => 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',
'form callback' => 'image_scale_form',
'summary theme' => 'image_scale_summary',
),
'image_scale_and_crop' => array(
'name' => t('Scale and Crop'),
'label' => 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',
'form callback' => 'image_resize_form',
'summary theme' => 'image_resize_summary',
),
'image_crop' => array(
'name' => t('Crop'),
'label' => t('Crop'),
'help' => t('Cropping will remove portions of an image to make it the specified dimensions.'),
'effect callback' => 'image_crop_effect',
'form callback' => 'image_crop_form',
'summary theme' => 'image_crop_summary',
),
'image_desaturate' => array(
'name' => t('Desaturate'),
'label' => t('Desaturate'),
'help' => t('Desaturate converts an image to grayscale.'),
'effect callback' => 'image_desaturate_effect',
),
'image_rotate' => array(
'name' => t('Rotate'),
'label' => 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',
'form callback' => 'image_rotate_form',
'summary theme' => 'image_rotate_summary',
),
);
......
......@@ -5,6 +5,7 @@ package = Core
version = VERSION
core = 7.x
files[] = image.module
files[] = image.admin.inc
files[] = image.effects.inc
files[] = image.install
files[] = image.test
......@@ -6,6 +6,33 @@
* Exposes global functionality for creating image styles.
*/
/**
* Implement of hook_help().
*/
function image_help($path, $arg) {
switch ($path) {
case 'admin/help#image':
$naming_approaches = array();
$naming_approaches[] = t('Based on where it will be used: !name', array('!name' => '<code>profile-picture</code>'));
$naming_approaches[] = t('Describing its appearance: !name', array('!name' => '<code>square-85x85</code>'));
$output = '';
$output .= '<p>' . t('The Image module provides functionality for displaying images on your site though <a href="!url">image styles</a>.', array('!url' => url('admin/settings/image-styles'))) .'</p>';
$output .= '<h3>' . t('Image styles') . '</h3>';
$output .= '<p>' . t('Image <em>styles</em> allow your site to output an image in several different ways without affecting the original image. Any created images will automatically be refreshed if any changes are made to the image style.') .'</p>';
$output .= '<p>' . t('Every image style must have a name, which will be used in the URL of generated images. There are two common approaches to naming image styles:') . '</p>';
$output .= theme('item_list', $naming_approaches);
$output .= '<p>' . t('Both approaches are common and which you choose depends on how you use the image style.') . '</p>';
$output .= '<p>' . t('After creating an image style, <em>effects</em> may be added to the style. Image module comes with some basic effects such as <em>crop</em>, <em>scale</em>, <em>desaturate</em>, and <em>rotate</em>. In addition to the effects included with Image, other modules may provide additional effects. Multiple effects may be combined together, such as using the <em>crop and scale</em> effect and the <em>desaturate</em> effect, you could create square, grayscale thumbnails.');
return $output;
case 'admin/settings/image-styles':
return '<p>' . t('Image styles commonly provide thumbnail sizes by scaling and cropping images, but can also add various effects before an image is displayed. When an image is displayed with a style, a new file is created and the original image is left unchanged.') . '</p>';
case 'admin/settings/image-styles/edit/%/add/%':
case 'admin/settings/image-styles/edit/%/effects/%':
$effect = ($arg[5] == 'add') ? image_effect_definition_load($arg[6]) : image_effect_load($arg[6]);
return isset($effect['help']) ? ('<p>' . $effect['help'] . '</p>') : NULL;
}
}
/**
* Implement hook_menu().
*/
......@@ -19,6 +46,79 @@ function image_menu() {
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
$items['admin/settings/image-styles'] = array(
'title' => 'Image styles',
'description' => 'Configure styles that can be used for resizing or adjusting images on display.',
'page callback' => 'image_style_list',
'access arguments' => array('administer image styles'),
);
$items['admin/settings/image-styles/list'] = array(
'title' => 'List',
'description' => 'List the current image styles on the site.',
'page callback' => 'image_style_list',
'access arguments' => array('administer image styles'),
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => 1,
);
$items['admin/settings/image-styles/add'] = array(
'title' => 'Add style',
'description' => 'Add a new image style.',
'page callback' => 'drupal_get_form',
'page arguments' => array('image_style_add_form'),
'access arguments' => array('administer image styles'),
'type' => MENU_LOCAL_TASK,
'weight' => 2,
);
$items['admin/settings/image-styles/edit/%image_style'] = array(
'title' => 'Edit style',
'title callback' => 'image_style_title',
'title arguments' => array('!name', 4),
'description' => 'Configure an image style.',
'page callback' => 'drupal_get_form',
'page arguments' => array('image_style_form', 4),
'access arguments' => array('administer image styles'),
'type' => MENU_CALLBACK,
);
$items['admin/settings/image-styles/delete/%image_style'] = array(
'title' => 'Delete style',
'title callback' => 'image_style_title',
'title arguments' => array('Delete !name', 4),
'description' => 'Delete an image style.',
'page callback' => 'drupal_get_form',
'page arguments' => array('image_style_delete_form', 4, TRUE),
'access arguments' => array('administer image styles'),
'type' => MENU_CALLBACK,
);
$items['admin/settings/image-styles/edit/%image_style/effects/%image_effect'] = array(
'title' => 'Edit image effect',
'title callback' => 'image_effect_title',
'title arguments' => array('!label effect', 6),
'description' => 'Edit an exiting effect within a style.',
'page callback' => 'drupal_get_form',
'page arguments' => array('image_effect_form', 4, 6),
'access arguments' => array('administer image styles'),
'type' => MENU_CALLBACK,
);
$items['admin/settings/image-styles/edit/%image_style/effects/%image_effect/delete'] = array(
'title' => 'Delete image effect',
'title callback' => 'image_effect_title',
'title arguments' => array('Delete !label', 6),
'description' => 'Delete an exiting effect from a style.',
'page callback' => 'drupal_get_form',
'page arguments' => array('image_effect_delete_form', 4, 6),
'access arguments' => array('administer image styles'),
'type' => MENU_CALLBACK,
);
$items['admin/settings/image-styles/edit/%image_style/add/%image_effect_definition'] = array(
'title' => 'Add image effect',
'title callback' => 'image_effect_title',
'title arguments' => array('Add !label effect', 6),
'description' => 'Add a new effect to a style.',
'page callback' => 'drupal_get_form',
'page arguments' => array('image_effect_form', 4, 6),
'access arguments' => array('administer image styles'),
'type' => MENU_CALLBACK,
);
return $items;
}
......@@ -38,6 +138,42 @@ function image_theme() {
'getsize' => TRUE,
),
),
'image_style_list' => array(
'arguments' => array('styles' => NULL),
),
'image_style_effects' => array(
'arguments' => array('form' => NULL),
),
'image_style_preview' => array(
'arguments' => array('style' => NULL),
),
'image_anchor' => array(
'arguments' => array('element' => NULL),
),
'image_resize_summary' => array(
'arguments' => array('data' => NULL),
),
'image_scale_summary' => array(
'arguments' => array('data' => NULL),
),
'image_crop_summary' => array(
'arguments' => array('data' => NULL),
),
'image_rotate_summary' => array(
'arguments' => array('data' => NULL),
),
);
}
/**
* Implement hook_permission().
*/
function image_permission() {
return array(
'administer image styles' => array(
'title' => t('Administer image styles'),
'description' => t('Create and modify styles for generating image modifications such as thumbnails.'),
),
);
}
......@@ -154,8 +290,8 @@ function image_styles() {
* 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.
* - "effects": An array of image effects within this image style.
* If the image style name or ID is not valid, an empty array is returned.
* @see image_effect_load()
*/
function image_style_load($name = NULL, $isid = NULL) {
......@@ -185,7 +321,7 @@ function image_style_load($name = NULL, $isid = NULL) {
* @param style
* An image style array.
* @return
* A style array. In the case of a new style, 'isid' will be populated.
* An image 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'])) {
......@@ -242,9 +378,9 @@ function image_style_delete($style, $replacement_style_name = '') {
* @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.
* An array of image effects associated with specified image 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();
......@@ -464,10 +600,10 @@ function image_style_path($style_name, $path) {
}
/**
* Pull in effects exposed by other modules using hook_image_effect_info().
* Pull in image effects exposed by modules implementing hook_image_effect_info().
*
* @return
* An array of effects to be used when transforming images.
* An array of image effects to be used when transforming images.
* @see hook_image_effect_info()
* @see image_effect_definition_load()
*/
......@@ -498,13 +634,13 @@ function image_effect_definitions() {
}
/**
* Load the definition for an effect.
* Load the definition for an image effect.
*
* The effect definition is a set of core properties for an effect, not
* The effect definition is a set of core properties for an image effect, not
* containing any user-settings. The definition defines various functions to
* call when configuring or executing an effect. This loader is mostly for
* call when configuring or executing an image 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.
* image_style_load() to get image effects that contain configuration.
*
* @param $effect
* The name of the effect definition to load.
......@@ -545,7 +681,7 @@ function image_effects() {
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.
// Do not load image effects whose definition cannot be found.
if ($definition) {
$effect = array_merge($definition, $effect);
$effects[$effect['ieid']] = $effect;
......@@ -564,10 +700,10 @@ function image_effects() {
* @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.
* - "isid": The unique image style ID that contains this image effect.
* - "weight": The weight of this image effect within the image style.
* - "name": The name of the effect definition that powers this image effect.
* - "data": An array of configuration options for this image 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.
......@@ -585,7 +721,7 @@ function image_effect_load($ieid) {
* @param $effect
* An image effect array.
* @return
* An image effect array. In the case of a new effect 'ieid' will be set.
* An image effect array. In the case of a new effect, 'ieid' will be set.
*/
function image_effect_save($effect) {
if (!empty($effect['ieid'])) {
......@@ -619,7 +755,7 @@ function image_effect_delete($effect) {
* @param $effect
* An image effect array.
* @return
* TRUE on success. FALSE if unable to perform effect on image.
* TRUE on success. FALSE if unable to perform the image effect on the image.
*/
function image_effect_apply($image, $effect) {
if (drupal_function_exists($effect['effect callback'])) {
......
......@@ -136,10 +136,10 @@ class ImageEffectsUnitTest extends ImageToolkitTestCase {
*/
function testEffects() {
$effects = image_effects();
$this->assertEqual(count($effects), 1, t("Found core's effect."));
$this->assertEqual(count($effects), 1, t("Found core's image effect."));
$effect_definitions = image_effect_definitions();
$this->assertEqual(count($effect_definitions), 6, t("Found core's effects."));
$this->assertEqual(count($effect_definitions), 6, t("Found core's image effects."));
}
/**
......@@ -226,3 +226,203 @@ class ImageEffectsUnitTest extends ImageToolkitTestCase {
$this->assertEqual($calls['rotate'][0][2], 0xffffff, t('Background color was passed correctly'));
}
}
/**
* Tests creation, deletion, and editing of image styles and effects.
*/
class ImageAdminStylesUnitTest extends DrupalWebTestCase {
function getInfo() {
return array(
'name' => 'Image styles and effects UI configuration',
'description' => 'Tests creation, deletion, and editing of image styles and effects at the UI level.',
'group' => 'Image',
);
}
/**
* Implementation of setUp().
*/
function setUp() {
parent::setUp();
// Create an administrative user.
$this->admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer image styles'));
$this->drupalLogin($this->admin_user);
}
/**
* Given an image style, generate an image.
*/
function createSampleImage($style) {
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_style_url($style['name'], $file_path) ? $file_path : FALSE;
}
/**
* Count the number of images currently create for a style.
*/
function getImageCount($style) {
$directory = file_directory_path() . '/styles/' . $style['name'];
return count(file_scan_directory($directory, '/.*/'));
}
/**
* General test to add a style, add/remove/edit effects to it, then delete it.
*/
function testStyle() {
// Setup a style to be created and effects to add to it.
$style_name = strtolower($this->randomName(10));
$style_path = 'admin/settings/image-styles/edit/' . $style_name;
$effect_edits = array(
'image_resize' => array(
'data[width]' => 100,
'data[height]' => 101,
),
'image_scale' => array(
'data[width]' => 110,
'data[height]' => 111,
'data[upscale]' => 1,
),
'image_scale_and_crop' => array(
'data[width]' => 120,
'data[height]' => 121,
),
'image_crop' => array(
'data[width]' => 130,
'data[height]' => 131,
'data[anchor]' => 'center-center',
),
'image_desaturate' => array(
// No options for desaturate.
),
'image_rotate' => array(
'data[degrees]' => 5,
'data[random]' => 1,
'data[bgcolor]' => '#FFFF00',
),
);
// Add style form.
$edit = array(
'name' => $style_name,
);
$this->drupalPost('admin/settings/image-styles/add', $edit, t('Create new style'));
$this->assertRaw(t('Style %name was created.', array('%name' => $style_name)), t('Image style successfully created.'));
// Add effect form.
// Add each sample effect to the style.
foreach ($effect_edits as $effect => $edit) {
// Add the effect.
$this->drupalPost($style_path, array('new' => $effect), t('Add'));
if (!empty($edit)) {
$this->drupalPost(NULL, $edit, t('Add effect'));
}
}
// Edit effect form.
// Revisit each form to make sure the effect was saved.
$style = image_style_load($style_name);
foreach ($style['effects'] as $ieid => $effect) {
$this->drupalGet($style_path . '/effects/' . $ieid);
foreach ($effect_edits[$effect['name']] as $field => $value) {
$this->assertFieldByName($field, $value, t('The %field field in the %effect effect has the correct value of %value.', array('%field' => $field, '%effect' => $effect['name'], '%value' => $value)));
}
}
// Image style overview form (ordering and renaming).
// Confirm the order of effects is maintained according to the order we
// added the fields.
$effect_edits_order = array_keys($effect_edits);
$effects_order = array_values($style['effects']);
$order_correct = TRUE;
foreach ($effects_order as $index => $effect) {
if ($effect_edits_order[$index] != $effect['name']) {
$order_correct = FALSE;
}
}
$this->assertTrue($order_correct, t('The order of the effects is correctly set by default.'));
// Test the style overview form.
// Change the name of the style and adjust the weights of effects.
$style_name = strtolower($this->randomName(10));
$weight = count($effect_edits);
$edit = array(
'name' => $style_name,
);
foreach ($style['effects'] as $ieid => $effect) {
$edit['effects[' . $ieid . '][weight]'] = $weight;
$weight--;
}
// Create an image to make sure it gets flushed after saving.
$image_path = $this->createSampleImage($style);
$this->assertEqual($this->getImageCount($style), 1, t('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path)));
$this->drupalPost($style_path, $edit, t('Update style'));
// Note that after changing the style name, the style path is changed.
$style_path = 'admin/settings/image-styles/edit/' . $style_name;
// Check that the URL was updated.
$this->drupalGet($style_path);
$this->assertResponse(200, t('Image style %original renamed to %new', array('%original' => $style['name'], '%new' => $style_name)));
// Check that the image was flushed after updating the style.
// This is especially important when renaming the style. Make sure that
// the old image directory has been deleted.
$this->assertEqual($this->getImageCount($style), 0, t('Image style %style was flushed after renaming the style and updating the order of effects.', array('%style' => $style['name'])));
// Load the style by the new name with the new weights.
drupal_static_reset('image_styles');
$style = image_style_load($style_name, NULL);
// Confirm the new style order was saved.
$effect_edits_order = array_reverse($effect_edits_order);
$effects_order = array_values($style['effects']);
$order_correct = TRUE;
foreach ($effects_order as $index => $effect) {
if ($effect_edits_order[$index] != $effect['name']) {
$order_correct = FALSE;
}
}
$this->assertTrue($order_correct, t('The order of the effects is correctly set by default.'));
// Image effect deletion form.
// Create an image to make sure it gets flushed after deleting an effect.
$image_path = $this->createSampleImage($style);
$this->assertEqual($this->getImageCount($style), 1, t('Image style %style image %file successfully generated.'