From 4e078b3af5889756267d6d820152998fe54437f8 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                  |   57 ++++++++++++--
 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(+), 45 deletions(-)

diff --git a/core/includes/image.inc b/core/includes/image.inc
index f6ae7f1..ee52686 100644
--- a/core/includes/image.inc
+++ b/core/includes/image.inc
@@ -291,11 +291,10 @@ function image_resize(stdClass $image, $width, $height) {
  * @param $degrees
  *   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.
+ *   An hexadecimal string specifying the background color to use for the
+ *   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,44 @@ function image_save(stdClass $image, $destination = NULL) {
 }
 
 /**
+ * Converts and tidies a hex string to RGBA (red, green, blue, alpha) integers.
+ *
+ * @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 tidied 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-7][0-9A-F])?$/', $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..052a262 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 $profile = 'testing';
+  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(array('image'));
+
+    // Source image width and 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 two 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), format_string('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, format_string('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, format_string('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'], format_string('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

