diff --git a/core/includes/image.inc b/core/includes/image.inc index 8dc36b9..7da9495 100644 --- a/core/includes/image.inc +++ b/core/includes/image.inc @@ -326,6 +326,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. @@ -334,7 +339,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; @@ -342,7 +347,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)); } /** @@ -438,5 +443,52 @@ function image_save(stdClass $image, $destination = NULL) { } /** + * Converts a hex string to RGBA (Red, Green, Blue, Alpha) integer values. + * + * @param $hex + * A string specifing an RGB color in the formats: + * '#ABC','ABC','#ABCD','ABCD','#AABBCC','AABBCC','#AABBCCDD','AABBCCDD' + * + * @return + * An array with four elements for red, green, blue, and alpha. + */ +function image_hex2rgba($hex) { + $hex = ltrim($hex, '#'); + if (preg_match('/^[0-9a-f]{3}$/i', $hex)) { + // 'FA3' is the same as 'FFAA33' so r=FF, g=AA, b=33 + $red = str_repeat($hex{0}, 2); + $green = str_repeat($hex{1}, 2); + $blue = str_repeat($hex{2}, 2); + $alpha = '0'; + } + elseif (preg_match('/^[0-9a-f]{6}$/i', $hex)) { + // #FFAA33 or r=FF, g=AA, b=33 + list($red, $green, $blue) = str_split($hex, 2); + $alpha = '0'; + } + elseif (preg_match('/^[0-9a-f]{8}$/i', $hex)) { + // #FFAA33 or r=FF, g=AA, b=33 + list($red, $green, $blue, $alpha) = str_split($hex, 2); + } + elseif (preg_match('/^[0-9a-f]{4}$/i', $hex)) { + // 'FA37' is the same as 'FFAA3377' so r=FF, g=AA, b=33, a=77 + $red = str_repeat($hex{0}, 2); + $green = str_repeat($hex{1}, 2); + $blue = str_repeat($hex{2}, 2); + $alpha = str_repeat($hex{3}, 2); + } + else { + // Error: invalide hex string. + return FALSE; + } + + $red = hexdec($r); + $green = hexdec($g); + $blue = hexdec($b); + $alpha = hexdec($a); + return array('red' => $red, 'green' => $green, 'blue' => $blue, 'alpha' => $alpha); +} + +/** * @} End of "defgroup image". */ diff --git a/core/modules/image/image.admin.inc b/core/modules/image/image.admin.inc index d72fdf4..49b3fde 100644 --- a/core/modules/image/image.admin.inc +++ b/core/modules/image/image.admin.inc @@ -588,6 +588,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'), + '#size' => 10, + '#maxlength' => 8, + '#default_value' => empty($data['background_color']) ? '' : $data['background_color'], + '#field_prefix' => '#', + '#description' => t('Hex string specifying the background color to use when cropping the image to dimensions that exceeds the source. If not provided, will use the default. Examples: "RGB", "RGBA", "RRGGBB", "RRGGBBAA".'), + ); return $form; } diff --git a/core/modules/image/image.effects.inc b/core/modules/image/image.effects.inc index ea898f9..eb4526f 100644 --- a/core/modules/image/image.effects.inc +++ b/core/modules/image/image.effects.inc @@ -185,7 +185,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; } diff --git a/core/modules/simpletest/tests/image.test b/core/modules/simpletest/tests/image.test index 60599be..e0c53f2 100644 --- a/core/modules/simpletest/tests/image.test +++ b/core/modules/simpletest/tests/image.test @@ -458,3 +458,129 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { } } + +/** + * Tests image cropping functionality. + */ +class ImageCropBackgroundTestCase extends DrupalWebTestCase { + + protected $image; + protected $isid1; + protected $isid2; + + public static function getInfo() { + return array( + 'name' => '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. + $resource = imagecreatetruecolor($width, $height); + + // Find extension. + $extension_png = image_type_to_extension(IMAGETYPE_PNG); + + // Fill the image with pure blue color. + imagefill($resource, 0, 0, imagecolorallocate($resource, 0, 0, 255)); + + // Create a image destination. + $file = 'public://' . user_password() . $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), t('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'] = t('Cropping center-to-center from 50x20 to 50x50 with red (#FF0000) color on additional area.'); + $style1['expected_color'] = t('red (#FF0000)'); + $style1['expected_color_map'] = array('red' => 255, 'green' => 0, 'blue' => 0, 'alpha' => 0); + $style2 = image_style_load(NULL, $this->isid2); + $style2['description'] = t('Cropping center-to-center from 50x20 to 50x50 with no color specified for additional area.'); + $style2['expected_color'] = t('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, t('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, t('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'], t('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..48c5e27 100644 --- a/core/modules/system/image.gd.inc +++ b/core/modules/system/image.gd.inc @@ -179,15 +179,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; }