Finishing up http://drupal.org/node/373613

From: andrew morton <drewish@katherinehouse.com>


---

 includes/image.inc                         |   28 ++++++++++
 modules/simpletest/tests/image.test        |   80 ++++++++++++++++++++++++----
 modules/simpletest/tests/image_test.module |   15 ++++-
 modules/system/image.gd.inc                |   76 +++++++++++++++++++++++++++
 modules/system/system.api.php              |    1 
 5 files changed, 186 insertions(+), 14 deletions(-)


diff --git includes/image.inc includes/image.inc
index 4adbee0..64ac64a 100644
--- includes/image.inc
+++ includes/image.inc
@@ -232,6 +232,7 @@ function image_scale(stdClass $image, $width = NULL, $height = NULL, $upscale =
  *   TRUE or FALSE, based on success.
  *
  * @see image_load()
+ * @see image_gd_resize()
  */
 function image_resize(stdClass $image, $width, $height) {
   $width = (int) round($width);
@@ -241,6 +242,29 @@ function image_resize(stdClass $image, $width, $height) {
 }
 
 /**
+ * Rotate an image by the given number of degrees.
+ *
+ * @param $image
+ *   An image object returned by image_load().
+ * @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.
+ * @return
+ *   TRUE or FALSE, based on success.
+ *
+ * @see image_load()
+ * @see image_gd_rotate()
+ */
+function image_rotate(stdClass $image, $degrees, $background = NULL) {
+  return image_toolkit_invoke('rotate', $image, array($degrees, $background));
+}
+
+/**
  * Crop an image to the rectangle specified by the given rectangle.
  *
  * @param $image
@@ -258,6 +282,7 @@ function image_resize(stdClass $image, $width, $height) {
  *
  * @see image_load()
  * @see image_scale_and_crop()
+ * @see image_gd_crop()
  */
 function image_crop(stdClass $image, $x, $y, $width, $height) {
   $aspect = $image->info['height'] / $image->info['width'];
@@ -279,6 +304,7 @@ function image_crop(stdClass $image, $x, $y, $width, $height) {
  *   TRUE or FALSE, based on success.
  *
  * @see image_load()
+ * @see image_gd_desaturate()
  */
 function image_desaturate(stdClass $image) {
   return image_toolkit_invoke('desaturate', $image);
@@ -307,6 +333,7 @@ function image_desaturate(stdClass $image) {
  * @see image_save()
  * @see image_get_info()
  * @see image_get_available_toolkits()
+ * @see image_gd_load()
  */
 function image_load($file, $toolkit = FALSE) {
   if (!$toolkit) {
@@ -337,6 +364,7 @@ function image_load($file, $toolkit = FALSE) {
  *   TRUE or FALSE, based on success.
  *
  * @see image_load()
+ * @see image_gd_save()
  */
 function image_save(stdClass $image, $destination = NULL) {
   if (empty($destination)) {
diff --git modules/simpletest/tests/image.test modules/simpletest/tests/image.test
index 0bdb6f1..192c766 100644
--- modules/simpletest/tests/image.test
+++ modules/simpletest/tests/image.test
@@ -146,6 +146,19 @@ class ImageToolkitTestCase extends DrupalWebTestCase {
   }
 
   /**
+   * Test the image_rotate() function.
+   */
+  function testRotate() {
+    $this->assertTrue(image_rotate($this->image, 90, 1), t('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], 1, t('Background color was passed correctly'));
+  }
+
+  /**
    * Test the image_crop() function.
    */
   function testCrop() {
@@ -193,7 +206,7 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase {
   function getInfo() {
     return array(
       'name' => t('Image GD manipulation tests'),
-      'description' => t('Check that core image manipulations work properly: scale, resize, crop, scale and crop, and desaturate.'),
+      'description' => t('Check that core image manipulations work properly: scale, resize, rotate, crop, scale and crop, and desaturate.'),
       'group' => t('Image API'),
     );
   }
@@ -304,18 +317,63 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase {
         'height' => 8,
         'corners' => array_fill(0, 4, $this->black),
       ),
-      'desaturate' => array(
-        'function' => 'desaturate',
-        'arguments' => array(),
-        'height' => 20,
-        'width' => 40,
-        // Grayscale corners are a bit funky. Each of the corners are a shade of
-        // gray. The values of these were determined simply by looking at the
-        // final image to see what desaturated colors end up being.
-        'corners' => array(array_fill(0, 3, 76) + array(3 => 0), array_fill(0, 3, 149) + array(3 => 0), array_fill(0, 3, 29) + array(3 => 0), array_fill(0, 3, 0) + array(3 => 127)),
-      ),
     );
 
+    // Systems using non-bundled GD2 don't have imagerotate. Test if available.
+    if (drupal_function_exists('imagerotate')) {
+      $operations += array(
+        'rotate_5' => array(
+          'function' => 'rotate',
+          'arguments' => array(5, 0xFF00FF), // Fuchsia background.
+          'width' => 42,
+          'height' => 24,
+          'corners' => array_fill(0, 4, $this->fuchsia),
+        ),
+        'rotate_90' => array(
+          'function' => 'rotate',
+          'arguments' => array(90, 0xFF00FF), // Fuchsia background.
+          'width' => 20,
+          'height' => 40,
+          'corners' => array($this->fuchsia, $this->red, $this->green, $this->blue),
+        ),
+        'rotate_transparent_5' => array(
+          'function' => 'rotate',
+          'arguments' => array(5),
+          'width' => 42,
+          'height' => 24,
+          'corners' => array_fill(0, 4, $this->transparent),
+        ),
+        'rotate_transparent_90' => array(
+          'function' => 'rotate',
+          'arguments' => array(90),
+          'width' => 20,
+          'height' => 40,
+          'corners' => array($this->transparent, $this->red, $this->green, $this->blue),
+        ),
+      );
+    }
+
+    // Systems using non-bundled GD2 don't have imagefilter. Test if available.
+    if (drupal_function_exists('imagefilter')) {
+      $operations += array(
+        'desaturate' => array(
+          'function' => 'desaturate',
+          'arguments' => array(),
+          'height' => 20,
+          'width' => 40,
+          // Grayscale corners are a bit funky. Each of the corners are a shade of
+          // gray. The values of these were determined simply by looking at the
+          // final image to see what desaturated colors end up being.
+          'corners' => array(
+            array_fill(0, 3, 76) + array(3 => 0),
+            array_fill(0, 3, 149) + array(3 => 0),
+            array_fill(0, 3, 29) + array(3 => 0),
+            array_fill(0, 3, 0) + array(3 => 127)
+          ),
+        ),
+      );
+    }
+
     foreach ($files as $file) {
       foreach ($operations as $op => $values) {
         // Load up a fresh image.
diff --git modules/simpletest/tests/image_test.module modules/simpletest/tests/image_test.module
index ac284ee..56a29bc 100644
--- modules/simpletest/tests/image_test.module
+++ modules/simpletest/tests/image_test.module
@@ -34,6 +34,7 @@ function image_test_reset() {
     'save' => array(),
     'settings' => array(),
     'resize' => array(),
+    'rotate' => array(),
     'crop' => array(),
     'desaturate' => array(),
   );
@@ -46,8 +47,8 @@ function image_test_reset() {
  *
  * @return
  *   An array keyed by operation name ('load', 'save', 'settings', 'resize',
- *   'crop', 'desaturate') with values being arrays of parameters passed to
- *   each call.
+ *   'rotate', 'crop', 'desaturate') with values being arrays of parameters
+ *   passed to each call.
  */
 function image_test_get_all_calls() {
   return variable_get('image_test_results', array());
@@ -58,7 +59,7 @@ function image_test_get_all_calls() {
  *
  * @param $op
  *   One of the image toolkit operations: 'load', 'save', 'settings', 'resize',
- *   'crop', 'desaturate'.
+ *   'rotate', 'crop', 'desaturate'.
  * @param $args
  *   Values passed to hook.
  * @see image_test_get_all_calls()
@@ -113,6 +114,14 @@ function image_test_resize(stdClass $image, $width, $height) {
 }
 
 /**
+ * Image tookit's rotate operation.
+ */
+function image_test_rotate(stdClass $image, $degrees, $background = NULL) {
+  _image_test_log_call('rotate', array($image, $degrees, $background));
+  return TRUE;
+}
+
+/**
  * Image tookit's desaturate operation.
  */
 function image_test_desaturate(stdClass $image) {
diff --git modules/system/image.gd.inc modules/system/image.gd.inc
index 0922ba6..71a3e9f 100644
--- modules/system/image.gd.inc
+++ modules/system/image.gd.inc
@@ -98,6 +98,76 @@ function image_gd_resize(stdClass $image, $width, $height) {
 }
 
 /**
+ * Rotate an image the given number of degrees.
+ *
+ * @param $image
+ *   An image object. The $image->resource, $image->info['width'], and
+ *   $image->info['height'] values will be modified by this call.
+ * @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.
+ * @return
+ *   TRUE or FALSE, based on success.
+ *
+ * @see image_rotate()
+ */
+function image_gd_rotate(stdClass $image, $degrees, $background = NULL) {
+  // PHP installations using non-bundled GD do not have imagerotate.
+  if (!drupal_function_exists('imagerotate')) {
+    watchdog('image', 'The image %file could not be rotated because the imagerotate() function is not available in this PHP installation.', array('%file' => $image->source));
+    return FALSE;
+  }
+
+  $width = $image->info['width'];
+  $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);
+    }
+    $background = imagecolorallocatealpha($image->resource, $rgb[0], $rgb[1], $rgb[2], 0);
+  }
+  // Set the background color as transparent if $background is NULL.
+  else {
+    // Get the current transparent color.
+    $background = imagecolortransparent($image->resource);
+
+    // If no transparent colors, use white.
+    if ($background == 0) {
+      $background = imagecolorallocatealpha($image->resource, 255, 255, 255, 0);
+    }
+  }
+
+  // Images are assigned a new color pallete when rotating, removing any
+  // transparency flags. For GIF images, keep a record of the transparent color.
+  if ($image->info['extension'] == 'gif') {
+    $transparent_index = imagecolortransparent($image->resource);
+    if ($transparent_index != 0) {
+      $transparent_gif_color = imagecolorsforindex($image->resource, $transparent_index);
+    }
+  }
+
+  $image->resource = imagerotate($image->resource, 360 - $degrees, $background);
+
+  // GIFs need to reassign the transparent color after performing the rotate.
+  if (isset($transparent_gif_color)) {
+    $background = imagecolorexactalpha($image->resource, $transparent_gif_color['red'], $transparent_gif_color['green'], $transparent_gif_color['blue'], $transparent_gif_color['alpha']);
+    imagecolortransparent($image->resource, $background);
+  }
+
+  $image->info['width'] = imagesx($image->resource);
+  $image->info['height'] = imagesy($image->resource);
+  return TRUE;
+}
+
+/**
  * Crop an image using the GD toolkit.
  *
  * @param $image
@@ -144,6 +214,12 @@ function image_gd_crop(stdClass $image, $x, $y, $width, $height) {
  * @see image_desaturate()
  */
 function image_gd_desaturate(stdClass $image) {
+  // PHP installations using non-bundled GD do not have imagefilter.
+  if (!drupal_function_exists('imagefilter')) {
+    watchdog('image', 'The image %file could not be rotated because the imagefilter() function is not available in this PHP installation.', array('%file' => $image->source));
+    return FALSE;
+  }
+
   return imagefilter($image->resource, IMG_FILTER_GRAYSCALE);
 }
 
diff --git modules/system/system.api.php modules/system/system.api.php
index 9a50e43..65c871c 100644
--- modules/system/system.api.php
+++ modules/system/system.api.php
@@ -377,6 +377,7 @@ function hook_init() {
  *   - 'save': Required. See image_gd_save() for usage.
  *   - 'settings': Optional. See image_gd_settings() for usage.
  *   - 'resize': Optional. See image_gd_resize() for usage.
+ *   - 'rotate': Optional. See image_gd_rotate() for usage.
  *   - 'crop': Optional. See image_gd_crop() for usage.
  *   - 'desaturate': Optional. See image_gd_desaturate() for usage.
  *
