Index: modules/system/system.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v
retrieving revision 1.22
diff -u -r1.22 system.api.php
--- modules/system/system.api.php	9 Mar 2009 11:44:54 -0000	1.22
+++ modules/system/system.api.php	10 Mar 2009 02:11:10 -0000
@@ -377,6 +377,7 @@
  *   - '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.
  *
Index: modules/system/image.gd.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/image.gd.inc,v
retrieving revision 1.4
diff -u -r1.4 image.gd.inc
--- modules/system/image.gd.inc	9 Mar 2009 11:44:54 -0000	1.4
+++ modules/system/image.gd.inc	10 Mar 2009 02:11:09 -0000
@@ -98,6 +98,72 @@
 }
 
 /**
+ * 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 $degress
+ *   The number of degrees to rotate the image.
+ * @param $background
+ *   The color of the exposed background when rotating.
+ * @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 +210,12 @@
  * @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);
 }
 
Index: modules/simpletest/tests/image.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/image.test,v
retrieving revision 1.1
diff -u -r1.1 image.test
--- modules/simpletest/tests/image.test	9 Mar 2009 11:44:54 -0000	1.1
+++ modules/simpletest/tests/image.test	10 Mar 2009 02:11:09 -0000
@@ -146,6 +146,19 @@
   }
 
   /**
+   * 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 @@
   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,58 @@
         '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.
Index: modules/simpletest/tests/image_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/image_test.module,v
retrieving revision 1.1
diff -u -r1.1 image_test.module
--- modules/simpletest/tests/image_test.module	9 Mar 2009 11:44:54 -0000	1.1
+++ modules/simpletest/tests/image_test.module	10 Mar 2009 02:11:09 -0000
@@ -34,6 +34,7 @@
     'save' => array(),
     'settings' => array(),
     'resize' => array(),
+    'rotate' => array(),
     'crop' => array(),
     'desaturate' => array(),
   );
@@ -46,8 +47,8 @@
  *
  * @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 @@
  *
  * @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 @@
 }
 
 /**
+ * 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) {
Index: includes/image.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/image.inc,v
retrieving revision 1.29
diff -u -r1.29 image.inc
--- includes/image.inc	9 Mar 2009 11:44:54 -0000	1.29
+++ includes/image.inc	10 Mar 2009 02:11:08 -0000
@@ -241,6 +241,28 @@
 }
 
 /**
+ * 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()
+ */
+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
