From 6d6a56f7a88a634ebb61b8a4aa06edc9e6e514d2 Mon Sep 17 00:00:00 2001
From: Claudiu Cristea <clau.cristea@gmail.com>
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                  |   65 ++++++++++++++--
 core/modules/image/image.admin.inc       |   24 ++++--
 core/modules/image/image.effects.inc     |   19 +----
 core/modules/image/image.test            |    4 +-
 core/modules/simpletest/tests/image.test |  130 +++++++++++++++++++++++++++++-
 core/modules/system/image.gd.inc         |   29 +++++--
 6 files changed, 232 insertions(+), 39 deletions(-)

diff --git a/core/includes/image.inc b/core/includes/image.inc
index f6ae7f1..879d8bf 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,54 @@ 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 associative array having 'red', 'green', 'blue' and 'alpha' as keys and
+ *   corresponding color decimal integer as values.
+ */
+function image_hex2rgba($hex) {
+  $hex = ltrim($hex, '#');
+  if (preg_match('/^[0-9a-f]{3}$/i', $hex)) {
+    // Sample 'FA3'. Same as 'FFAA33'.
+    $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)) {
+    // Sample 'FFAA33'.
+    list($red, $green, $blue) = str_split($hex, 2);
+    $alpha = '0';
+  }
+  elseif (preg_match('/^[0-9a-f]{8}$/i', $hex)) {
+    // Sample 'FFAA3300'.
+    list($red, $green, $blue, $alpha) = str_split($hex, 2);
+  }
+  elseif (preg_match('/^[0-9a-f]{4}$/i', $hex)) {
+    // Sample 'FA37'. Same as 'FFAA3377'.
+    $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: Invalid hex string.
+    return FALSE;
+  }
+
+  // Convert colors to decimal integers.
+  $red = hexdec($red);
+  $green = hexdec($green);
+  $blue = hexdec($blue);
+  $alpha = hexdec($alpha);
+  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 85712e6..d44e4d4 100644
--- a/core/modules/image/image.admin.inc
+++ b/core/modules/image/image.admin.inc
@@ -476,9 +476,8 @@ function image_effect_integer_validate($element, &$form_state) {
  */
 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'])));
+    if (FALSE === image_hex2rgba($element['#value'])) {
+      form_error($element, t('!name must be a hexadecimal color value. Examples: "RGB", "RGBA", "RRGGBB", "RRGGBBAA".', array('!name' => $element['#title'])));
     }
   }
 }
@@ -587,6 +586,16 @@ 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.'),
+    '#size' => 10,
+    '#maxlength' => 8,
+    '#default_value' => empty($data['background_color']) ? '' : $data['background_color'],
+    '#field_prefix' => '#',
+    '#element_validate' => array('image_effect_color_validate'),
+  );
 
   return $form;
 }
@@ -616,11 +625,12 @@ function image_rotate_form($data) {
   );
   $form['bgcolor'] = array(
     '#type' => 'textfield',
-    '#default_value' => (isset($data['bgcolor'])) ? $data['bgcolor'] : '#FFFFFF',
+    '#default_value' => empty($data['bgcolor']) ? '' : ltrim($data['bgcolor'], '#'),
     '#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.'),
+    '#size' => 10,
+    '#maxlength' => 8,
+    '#field_prefix' => '#',
     '#element_validate' => array('image_effect_color_validate'),
   );
   $form['random'] = array(
diff --git a/core/modules/image/image.effects.inc b/core/modules/image/image.effects.inc
index 35a6a74..603a7e0 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 : ltrim($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 0fc2a4c..503a756 100644
--- a/core/modules/image/image.test
+++ b/core/modules/image/image.test
@@ -313,7 +313,7 @@ class ImageEffectsUnitTest extends ImageToolkitTestCase {
     // 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', t('Background color was passed correctly'));
   }
 }
 
@@ -386,7 +386,7 @@ class ImageAdminStylesUnitTest extends ImageFieldTestCase {
       'image_rotate' => array(
         'data[degrees]' => 5,
         'data[random]' => 1,
-        'data[bgcolor]' => '#FFFF00',
+        'data[bgcolor]' => 'FFFF00',
       ),
     );
 
diff --git a/core/modules/simpletest/tests/image.test b/core/modules/simpletest/tests/image.test
index daf5394..90a6716 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),
@@ -462,3 +462,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".<br />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..9a6e191 100644
--- a/core/modules/system/image.gd.inc
+++ b/core/modules/system/image.gd.inc
@@ -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

