From ab39efd17d866f847ea93d4c9056d8b517c51f3a Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Tue, 8 Nov 2011 11:25:14 +0200 Subject: [PATCH] Issue #204497 by claudiu.cristea, Murz, Rob Loach, dman, tylor, edmund.kwok, sun | FiReaNG3L: Added Configurable background color when cropping to larger dimensions. --- core/includes/image.inc | 56 ++++++++++++-- core/modules/image/image.admin.inc | 30 +++++-- core/modules/image/image.effects.inc | 19 +---- core/modules/image/image.test | 4 +- core/modules/simpletest/tests/image.test | 124 +++++++++++++++++++++++++++++- core/modules/system/image.gd.inc | 37 ++++++--- 6 files changed, 226 insertions(+), 44 deletions(-) diff --git a/core/includes/image.inc b/core/includes/image.inc index f6ae7f1..39afe6d 100644 --- a/core/includes/image.inc +++ b/core/includes/image.inc @@ -292,10 +292,9 @@ function image_resize(stdClass $image, $width, $height) { * The number of (clockwise) degrees to rotate the image. * @param $background * An hexadecimal integer specifying the background color to use for the - * uncovered area of the image after the rotation. E.g. 0x000000 for black, - * 0xff00ff for magenta, and 0xffffff for white. For images that support - * transparency, this will default to transparent. Otherwise it will - * be white. + * uncovered area of the image after the rotation. Examples: "#RGB", "#RGBA", + * "#RRGGBB", "#RRGGBBAA". For images that support transparency, this will + * default to transparent. Otherwise it will be white. * * @return * TRUE on success, FALSE on failure. @@ -320,6 +319,11 @@ function image_rotate(stdClass $image, $degrees, $background = NULL) { * The target width, in pixels. * @param $height * The target height, in pixels. + * @param $background + * An hexadecimal string specifying the background color to use for the + * additional area created when cropping to larger dimensions than the source. + * Examples: "#RGB", "#RGBA", "#RRGGBB", "#RRGGBBAA". For images that support + * transparency, this will default to transparent. Otherwise it will be white. * * @return * TRUE on success, FALSE on failure. @@ -328,7 +332,7 @@ function image_rotate(stdClass $image, $degrees, $background = NULL) { * @see image_scale_and_crop() * @see image_gd_crop() */ -function image_crop(stdClass $image, $x, $y, $width, $height) { +function image_crop(stdClass $image, $x, $y, $width, $height, $background = NULL) { $aspect = $image->info['height'] / $image->info['width']; if (empty($height)) $height = $width / $aspect; if (empty($width)) $width = $height * $aspect; @@ -336,7 +340,7 @@ function image_crop(stdClass $image, $x, $y, $width, $height) { $width = (int) round($width); $height = (int) round($height); - return image_toolkit_invoke('crop', $image, array($x, $y, $width, $height)); + return image_toolkit_invoke('crop', $image, array($x, $y, $width, $height, $background)); } /** @@ -432,5 +436,45 @@ function image_save(stdClass $image, $destination = NULL) { } /** + * Converts a hex string to RGBA (red, green, blue, alpha) integer values and + * tidy up the input hex color string. + * + * @param $hex + * A string specifing an RGB color in the formats: '#RGB', 'RGB', '#RGBA', + * 'RGBA', '#RRGGBB', 'RRGGBB', '#RRGGBBAA', 'RRGGBBAA'. The color is passed + * by reference in order to return a tidy up color string for consistency. + * + * @return + * An associative array having 'red', 'green', 'blue' and 'alpha' as keys and + * corresponding color decimal integer as values or FALSE on an invalid color + * hex string. + */ +function image_hex2rgba(&$hex) { + // Save a uppercase version without leading "#" for later processing. + $hex_color = drupal_strtoupper(ltrim($hex, '#')); + + // Assure consistency for the color string. + $hex = '#' . $hex_color; + + // Normalize shorthand versions. + // '#FA3' will become '#FFAA33', '#FA37' will become '#FFAA3377'. + if (strlen($hex_color) <= 4) { + $hex_color = preg_replace('|([0-9A-F])|', "\\1\\1", $hex_color); + } + + // Return FALSE if is not a valid hex color string. + if (!preg_match('/^([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})?$/', $hex_color, $colors)) { + return FALSE; + } + + return array( + 'red' => hexdec($colors[1]), + 'green' => hexdec($colors[2]), + 'blue' => hexdec($colors[3]), + 'alpha' => isset($colors[4]) ? hexdec($colors[4]) : 0, + ); +} + +/** * @} End of "defgroup image". */ diff --git a/core/modules/image/image.admin.inc b/core/modules/image/image.admin.inc index 9643841..16bd842 100644 --- a/core/modules/image/image.admin.inc +++ b/core/modules/image/image.admin.inc @@ -475,10 +475,15 @@ function image_effect_integer_validate($element, &$form_state) { * Element validate handler to ensure a hexadecimal color value. */ function image_effect_color_validate($element, &$form_state) { - if ($element['#value'] != '') { - $hex_value = preg_replace('/^#/', '', $element['#value']); - if (!preg_match('/^#[0-9A-F]{3}([0-9A-F]{3})?$/', $element['#value'])) { - form_error($element, t('!name must be a hexadecimal color value.', array('!name' => $element['#title']))); + $hex = $element['#value']; + if ($hex != '') { + if (FALSE === image_hex2rgba($hex)) { + form_error($element, t('%name must be a hexadecimal color value. Examples: "#RGB", "#RGBA", "#RRGGBB", "#RRGGBBAA".', array('%name' => $element['#title']))); + } + + // Update the submmitted value only if it was tidied up. + if ($hex != $element['#value']) { + form_set_value($element, $hex, $form_state); } } } @@ -587,6 +592,15 @@ function image_crop_form($data) { '#default_value' => $data['anchor'], '#description' => t('The part of the image that will be retained during the crop.'), ); + $form['background_color'] = array( + '#type' => 'textfield', + '#title' => t('Crop background color'), + '#description' => t('Hex string specifying the background color to use when cropping the image to dimensions that are exceeding the source dimensions. Examples: "#RGB", "#RGBA", "#RRGGBB", "#RRGGBBAA". Leave blank for transparency on image types that support it.'), + '#default_value' => empty($data['background_color']) ? '' : $data['background_color'], + '#element_validate' => array('image_effect_color_validate'), + '#size' => 12, + '#maxlength' => 9, + ); return $form; } @@ -616,12 +630,12 @@ function image_rotate_form($data) { ); $form['bgcolor'] = array( '#type' => 'textfield', - '#default_value' => (isset($data['bgcolor'])) ? $data['bgcolor'] : '#FFFFFF', '#title' => t('Background color'), - '#description' => t('The background color to use for exposed areas of the image. Use web-style hex colors (#FFFFFF for white, #000000 for black). Leave blank for transparency on image types that support it.'), - '#size' => 7, - '#maxlength' => 7, + '#description' => t('Hex string specifying the background color to use for exposed areas of the image. Examples: "#RGB", "#RGBA", "#RRGGBB", "#RRGGBBAA". Leave blank for transparency on image types that support it.'), + '#default_value' => empty($data['bgcolor']) ? '' : $data['bgcolor'], '#element_validate' => array('image_effect_color_validate'), + '#size' => 12, + '#maxlength' => 9, ); $form['random'] = array( '#type' => 'checkbox', diff --git a/core/modules/image/image.effects.inc b/core/modules/image/image.effects.inc index 35a6a74..2690844 100644 --- a/core/modules/image/image.effects.inc +++ b/core/modules/image/image.effects.inc @@ -183,7 +183,8 @@ function image_crop_effect(&$image, $data) { 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'])) { + $background_color = empty($data['background_color']) ? NULL : $data['background_color']; + if (!image_crop($image, $x, $y, $data['width'], $data['height'], $background_color)) { 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['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } @@ -258,26 +259,14 @@ function image_rotate_effect(&$image, $data) { '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; - } + $background_color = empty($data['bgcolor']) ? NULL : $data['bgcolor']; if (!empty($data['random'])) { $degrees = abs((float) $data['degrees']); $data['degrees'] = rand(-1 * $degrees, $degrees); } - if (!image_rotate($image, $data['degrees'], $data['bgcolor'])) { + if (!image_rotate($image, $data['degrees'], $background_color)) { 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['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } diff --git a/core/modules/image/image.test b/core/modules/image/image.test index 4d4532c..b1c6ba2 100644 --- a/core/modules/image/image.test +++ b/core/modules/image/image.test @@ -307,13 +307,13 @@ class ImageEffectsUnitTest extends ImageToolkitTestCase { */ 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->assertTrue(image_rotate_effect($this->image, array('degrees' => 90, 'bgcolor' => '#FFF')), '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')); + $this->assertEqual($calls['rotate'][0][2], '#FFF', 'Background color was passed correctly'); } } diff --git a/core/modules/simpletest/tests/image.test b/core/modules/simpletest/tests/image.test index ab008e5..1a798a1 100644 --- a/core/modules/simpletest/tests/image.test +++ b/core/modules/simpletest/tests/image.test @@ -333,14 +333,14 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { $operations += array( 'rotate_5' => array( 'function' => 'rotate', - 'arguments' => array(5, 0xFF00FF), // Fuchsia background. + 'arguments' => array(5, '#FF00FF'), // Fuchsia background. 'width' => 42, 'height' => 24, 'corners' => array_fill(0, 4, $this->fuchsia), ), 'rotate_90' => array( 'function' => 'rotate', - 'arguments' => array(90, 0xFF00FF), // Fuchsia background. + 'arguments' => array(90, '#FF00FF'), // Fuchsia background. 'width' => 20, 'height' => 40, 'corners' => array($this->fuchsia, $this->red, $this->green, $this->blue), @@ -503,3 +503,123 @@ class ImageFileMoveTest extends ImageToolkitTestCase { } } +/** + * Tests image cropping functionality. + */ +class ImageCropBackgroundTestCase extends DrupalWebTestCase { + + protected $image; + protected $isid1; + protected $isid2; + + public static function getInfo() { + return array( + 'name' => 'Image crop background color', + 'description' => 'Check if after cropping to larger dimensions, the uncovered area has the correct color.', + 'group' => 'Image', + ); + } + + function setUp() { + + // Invoke parent method. + parent::setUp(); + + // Source image width & height. + $width = 50; + $height = 25; + + // Create a new non-squared image filled with pure blue color. + $resource = imagecreatetruecolor($width, $height); + imagefill($resource, 0, 0, imagecolorallocate($resource, 0, 0, 255)); + + // Create an image destination. + $extension_png = image_type_to_extension(IMAGETYPE_PNG); + $file = 'public://' . $this->randomName() . $extension_png; + + // Save test image to disk. + $this->image = (object) array( + 'source' => $file, + 'resource' => $resource, + 'info' => array( + 'width' => $width, + 'height' => $height, + 'extension' => ltrim($extension_png, '.'), + 'mime_type' => image_type_to_mime_type(IMAGETYPE_PNG), + ), + 'toolkit' => image_get_toolkit(), + ); + image_save($this->image, $file); + + // Create 2 dummy styles. + $style1 = image_style_save(array('name' => 'color')); + $this->isid1 = $style1['isid']; + $style2 = image_style_save(array('name' => 'no-color')); + $this->isid2 = $style2['isid']; + + // Attach a single "image_crop" effect to the first style that enlarges the + // source image to add additional area filled with red (#FF0000). + $effect = array( + 'isid' => $this->isid1, + 'weight' => 0, + 'name' => 'image_crop', + 'data' => array('width' => 50, 'height' => 50, 'anchor' => 'center-center', 'background_color' => '#FF0000'), + ); + image_effect_save($effect); + + // Attach a single "image_crop" effect to the second style that enlarges the + // source image to add additional area with no specified fill. + $effect = array( + 'isid' => $this->isid2, + 'weight' => 0, + 'name' => 'image_crop', + 'data' => array('width' => 50, 'height' => 50, 'anchor' => 'center-center', 'background_color' => ''), + ); + image_effect_save($effect); + } + + /** + * Tests that image cropping uses the correct background color. + */ + function testCropBackgroundColor() { + // Check if physical image file is really created. + $this->assertTrue(is_file($this->image->source), 'The source image (50x25) filled with blue (#0000FF) has been created: %url.', array('%url' => $this->image->source)); + + $style1 = image_style_load(NULL, $this->isid1); + $style1['description'] = 'Cropping center-to-center from 50x20 to 50x50 with red (#FF0000) color on additional area.'; + $style1['expected_color'] = 'red (#FF0000)'; + $style1['expected_color_map'] = array('red' => 255, 'green' => 0, 'blue' => 0, 'alpha' => 0); + $style2 = image_style_load(NULL, $this->isid2); + $style2['description'] = 'Cropping center-to-center from 50x20 to 50x50 with no color specified for additional area.'; + $style2['expected_color'] = 'transparent'; + $style2['expected_color_map'] = array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 127); + + $styles = array( + $style1['name'] => $style1, + $style2['name'] => $style2, + ); + + foreach ($styles as $style_name => $style) { + $name = array('%name' => $style_name); + $color = array('%color' => $style['expected_color']); + $uri = image_style_path($style_name, $this->image->source); + + // Check if the Image Style has been created. + $this->assertTrue($style, 'The Image Style "%name" has been created and is usable.', $name); + + // Inform on current operation. + $this->assertTrue(TRUE, $style['description']); + + // Create derivative with a simple GET. + $this->drupalGet(image_style_url($style_name, $this->image->source)); + + // Load image as resource to inspect the pixel from (5, 5) which is in the + // new added area. + $this->assert(TRUE, 'Checking a pixel color from the new area, at (5, 5) in "%file". Expecting %color.', $color + array('%file' => $uri)); + $image = image_load($uri); + $rgb = imagecolorat($image->resource, 5, 5); + $colors = imagecolorsforindex($image->resource, $rgb); + $this->assertIdentical($colors, $style['expected_color_map'], 'Pixel from (5, 5) is %color', $color); + } + } +} diff --git a/core/modules/system/image.gd.inc b/core/modules/system/image.gd.inc index 39f86dc..9e5da25 100644 --- a/core/modules/system/image.gd.inc +++ b/core/modules/system/image.gd.inc @@ -105,10 +105,10 @@ function image_gd_resize(stdClass $image, $width, $height) { * The number of (clockwise) degrees to rotate the image. * @param $background * An hexadecimal integer specifying the background color to use for the - * uncovered area of the image after the rotation. E.g. 0x000000 for black, - * 0xff00ff for magenta, and 0xffffff for white. For images that support - * transparency, this will default to transparent. Otherwise it will - * be white. + * uncovered area of the image after the rotation. Examples: "#RGB", "#RGBA", + * "#RRGGBB", "#RRGGBBAA". For images that support transparency, this will + * default to transparent. Otherwise it will be white. + * * @return * TRUE or FALSE, based on success. * @@ -125,12 +125,10 @@ function image_gd_rotate(stdClass $image, $degrees, $background = NULL) { $height = $image->info['height']; // Convert the hexadecimal background value to a color index value. - if (isset($background)) { - $rgb = array(); - for ($i = 16; $i >= 0; $i -= 8) { - $rgb[] = (($background >> $i) & 0xFF); + if (!empty($background)) { + if ($background = image_hex2rgba($background)) { + $background = imagecolorallocatealpha($image->resource, $background['red'], $background['green'], $background['blue'], $background['alpha']); } - $background = imagecolorallocatealpha($image->resource, $rgb[0], $rgb[1], $rgb[2], 0); } // Set the background color as transparent if $background is NULL. else { @@ -179,15 +177,32 @@ function image_gd_rotate(stdClass $image, $degrees, $background = NULL) { * The width of the cropped area, in pixels. * @param $height * The height of the cropped area, in pixels. + * @param $background + * An hexadecimal string specifying the background color to use for the + * additional area created when cropping to larger dimensions than the source. + * Examples: "#RGB", "#RGBA", "#RRGGBB", "#RRGGBBAA". For images that support + * transparency, this will default to transparent. Otherwise it will be white. + * * @return * TRUE or FALSE, based on success. * * @see image_crop() */ -function image_gd_crop(stdClass $image, $x, $y, $width, $height) { +function image_gd_crop(stdClass $image, $x, $y, $width, $height, $background = NULL) { $res = image_gd_create_tmp($image, $width, $height); - if (!imagecopyresampled($res, $image->resource, 0, 0, $x, $y, $width, $height, $width, $height)) { + // Fill the background color if desired. + if (!empty($background)) { + if ($background = image_hex2rgba($background)) { + $background = imagecolorallocatealpha($res, $background['red'], $background['green'], $background['blue'], $background['alpha']); + imagefill($res, 0, 0, $background); + } + } + + // Copy the source image to our new destination image. We use + // $image->info['width'] instead of $width because we are copying using the + // source image's width and height, not the destination width and height. + if (!imagecopyresampled($res, $image->resource, -$x, -$y, 0, 0, $image->info['width'], $image->info['height'], $image->info['width'], $image->info['height'])) { return FALSE; } -- 1.7.5.4