Index: node_gallery.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/node_gallery/node_gallery.admin.inc,v
retrieving revision 1.11.2.51
diff -u -p -r1.11.2.51 node_gallery.admin.inc
--- node_gallery.admin.inc	22 Dec 2010 17:34:27 -0000	1.11.2.51
+++ node_gallery.admin.inc	29 Dec 2010 10:47:49 -0000
@@ -377,6 +377,14 @@ function node_gallery_relationship_setti
     '#default_value' => $relationship['settings']['manage_images_show_gallery_list'],
     '#description' => t('If checked, a gallery dropdown list will be displayed on each image on the Manage Images tab allowing you to move images between galleries.'),
   );
+  if (imageapi_default_toolkit() != 'imageapi_gd' || function_exists("imagerotate")) {
+    $form['node_gallery_manage_images_settings']['manage_images_enable_rotation'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Enable the rotation of images on the manage images page'),
+      '#default_value' => $relationship['settings']['manage_images_enable_rotation'],
+      '#description' => t('If checked, image rotation operations will be displayed on each image on the Manage Images tab.'),
+    );
+  }
   $form['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
Index: node_gallery.css
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/node_gallery/node_gallery.css,v
retrieving revision 1.4.2.12
diff -u -p -r1.4.2.12 node_gallery.css
--- node_gallery.css	18 Dec 2010 02:45:56 -0000	1.4.2.12
+++ node_gallery.css	29 Dec 2010 10:47:49 -0000
@@ -9,8 +9,47 @@ table.image-navigator td.image-navigator
 table.image-navigator td.image-navigator-last {width:15%; text-align:left}
 table.image-navigator td.image-navigator-gallery-link {width:20%; text-align:right}
 
+/**
+ * Helper classes *not* meant for styling.
+ */
 .ng3-hidden { display:none; }
 
+.ng3-rotate-90 {
+  transform: rotate(90deg);
+  -moz-transform:rotate(90deg);
+  -webkit-transform:rotate(90deg);
+  -o-transform:rotate(90deg);
+  filter:progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11=0, M12=-1,M21=1, M22=0);
+  -ms-filter:progid:DXImageTransform.Microsoft.Matrix(M11=0, M12=-1, M21=1, M22=0,sizingMethod='auto expand');
+}
+
+.ng3-rotate-180 {
+  transform:rotate(180deg);
+  -moz-transform:rotate(180deg);
+  -webkit-transform:rotate(180deg);
+  -o-transform:rotate(180deg);
+  filter:progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11=-1, M12=0,M21=0, M22=-1);
+  -ms-filter:progid:DXImageTransform.Microsoft.Matrix(M11=-1, M12=0, M21=0, M22=-1,sizingMethod='auto expand');
+}
+
+.ng3-rotate-270 {
+  transform:rotate(-90deg);
+  -moz-transform:rotate(-90deg);
+  -webkit-transform:rotate(-90deg);
+  -o-transform:rotate(-90deg);
+  filter:progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11=0, M12=1,M21=-1, M22=0);
+  -ms-filter:progid:DXImageTransform.Microsoft.Matrix(M11=0, M12=1, M21=-1, M22=0,sizingMethod='auto expand');
+}
+
+#node-gallery-rotate-dialog img {
+  margin: 10px;
+  border: 1px solid #000;
+}
+
+#node-gallery-rotate-dialog img.selected {
+  outline: 5px solid #000;
+}
+
 /**
  * Fix for word-wrap on the manage images page.
  */
Index: node_gallery.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/node_gallery/node_gallery.inc,v
retrieving revision 1.2.2.95
diff -u -p -r1.2.2.95 node_gallery.inc
--- node_gallery.inc	22 Dec 2010 17:34:27 -0000	1.2.2.95
+++ node_gallery.inc	29 Dec 2010 10:47:49 -0000
@@ -152,6 +152,7 @@ function node_gallery_relationship_setti
     'manage_images_fields' => array('title' => 'title'),
     'manage_images_per_page' => 20,
     'manage_images_show_gallery_list' => TRUE,
+    'manage_images_enable_rotation' => TRUE,
   );
   drupal_alter('node_gallery_relationship_default_settings', $settings);
   return $settings;
@@ -1022,6 +1023,37 @@ function node_gallery_batch_sorting_fini
   drupal_set_message($message);
 }
 
+function node_gallery_batch_rotate_callback($filepath, $degrees, &$context) {
+  $result = 0;
+  $image = imageapi_image_open($filepath);
+  if ($image !== FALSE) {
+    if (imageapi_default_toolkit() == 'imageapi_imagemagick') {
+      $result = imageapi_image_rotate($image, $degrees, 0x000000);
+    } else {
+      $result = imageapi_image_rotate($image, $degrees);
+    }
+    global $conf;
+    $conf['imageapi_jpeg_quality'] = 100;
+    imageapi_image_close($image);
+    imagecache_image_flush($filepath);
+    // add a _r$i to the filename for cache distinction of rotated images
+    $filepath_new = preg_replace_callback(
+        "/(_r([0-9]+))?(\.(.+))$/",
+        create_function('$matches', '$i = ((int)$matches[2] > 0) ? ((int)$matches[2])+1 : 1; return "_r".$i.$matches[3];'),
+        $filepath);
+    $file = (object)field_file_load($filepath);
+    $file->timestamp = time();
+    $file->filepath = $filepath_new;
+    $file->filename = basename($filepath_new);
+    $filepath_old = $filepath;
+    file_move($filepath, $filepath_new);
+    drupal_write_record('files', $file, 'fid');
+  } else {
+    watchdog('node_gallery', t('Could not open image for rotation'));
+  }
+  $context['results'][] = $result;
+}
+
 /**
  * Compare taxonomy on an unsaved node object versus a node_load()ed object
  * @param $old
Index: node_gallery.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/node_gallery/node_gallery.module,v
retrieving revision 1.18.2.152
diff -u -p -r1.18.2.152 node_gallery.module
--- node_gallery.module	22 Dec 2010 17:34:27 -0000	1.18.2.152
+++ node_gallery.module	29 Dec 2010 10:47:49 -0000
@@ -866,6 +866,9 @@ function node_gallery_batch_node_save($n
 function node_gallery_image_process_finished($success, $results, $operations) {
   if ($success) {
     drupal_set_message(t('Image modifications complete.'));
+    cache_clear_all();
+    _field_file_cache(NULL, TRUE);
+    content_clear_type_cache();
   }
   else {
     // An error occurred.
Index: node_gallery.pages.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/node_gallery/node_gallery.pages.inc,v
retrieving revision 1.12.2.78
diff -u -p -r1.12.2.78 node_gallery.pages.inc
--- node_gallery.pages.inc	22 Dec 2010 17:34:27 -0000	1.12.2.78
+++ node_gallery.pages.inc	29 Dec 2010 10:55:45 -0000
@@ -191,7 +191,10 @@ function node_gallery_manage_images_form
     $images = $chunks[$pager_page_array[$element]];
   }
   $pager_total[$element] = count($chunks);
-
+  $enable_rotation = FALSE;
+  if ($relationship['settings']['manage_images_enable_rotation'] && (imageapi_default_toolkit() != 'imageapi_gd' || function_exists("imagerotate"))) {
+    $enable_rotation = TRUE;
+  }
   if (!empty($images)) {
     $count = 0;
     $form['images']['#tree'] = TRUE;
@@ -204,6 +207,22 @@ function node_gallery_manage_images_form
       $image = node_load($nid);
       $options[$nid] = '';
       $form['images'][$nid]['remove'] = array('#type' => 'checkbox', '#default_value' => $file->remove);
+      if ($enable_rotation) {
+        $form['images'][$nid]['rotate'] = array(
+          '#type' => 'radios',
+          '#options' => array(
+            0 => t('None'),
+            90 => t('90° CW'),
+            270 => t('90° CCW'),
+            180 => t('180°')
+          ),
+          '#default_value' => 0,
+        );
+        if (module_exists('jquery_ui')) {
+          // add a link to a jquery ui dialog with previews
+          $form['images'][$nid]['rotate']['#suffix'] = '<a href="#" class="ng3-rotate-link" rel="'.imagecache_create_path('node-gallery-thumbnail', $image->{$relationship['imagefield_name']}[0]['filepath']).'" />rotate</a>';
+        }
+      }
       if ($relationship['settings']['manage_images_show_gallery_list']) {
         $form['images'][$nid]['gid'] = array('#type' => 'select', '#title' => t('Gallery'), '#default_value' => $gallery->nid, '#options' => $gallery_list);
       }
@@ -314,6 +333,14 @@ function node_gallery_manage_images_subm
         $op_images['update'][] = $image_node;
       }
     }
+    if ($form_values['rotate']) {
+      $allowed_degrees = array('90', '270', '180');
+      $degrees = (int)$form_values['rotate'];
+      if (in_array($degrees, $allowed_degrees)) {
+        $node = $form['images'][$image_node->nid]['edit_form']['#node'];
+        $op_images['rotate'][] = array($node->{$relationship['imagefield_name']}[0]['filepath'], $degrees);
+      }
+    }
   }
   while (!empty($op_images['update'])) {
     $node = array_shift($op_images['update']);
@@ -323,6 +350,10 @@ function node_gallery_manage_images_subm
     $nid = array_shift($op_images['delete']);
     $operations[] = array('node_gallery_image_delete_process', array($nid));
   }
+  while (!empty($op_images['rotate'])) {
+    $params = array_shift($op_images['rotate']);
+    $operations[] = array('node_gallery_batch_rotate_callback', $params);
+  }
   if (!empty($operations)) {
     $batch = array(
       'operations' => $operations,
Index: js/ng_manage_images.js
===================================================================
RCS file: js/ng_manage_images.js
diff -N js/ng_manage_images.js
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ js/ng_manage_images.js	29 Dec 2010 10:56:06 -0000
@@ -0,0 +1,68 @@
+/**
+ * @file
+ * jQuery additions to manage images page.
+ */
+
+/**
+ * Image rotation preview.
+ */
+Drupal.theme.prototype.nodeGalleryRotateDialog = function (filepath) {
+  var output = '';
+  filepath = Drupal.settings.basePath + filepath;
+  output += '<img src="' + filepath + '" class="ng3-rotate-90" />';
+  output += '<img src="' + filepath + '" class="ng3-rotate-180" />';
+  output += '<img src="' + filepath + '" class="ng3-rotate-270" />';
+  return output;
+};
+
+Drupal.behaviors.nodeGalleryRotateImagePreview = function (context) {
+  var self = this;
+  this.link = undefined;
+
+  var buttonOk = function () {
+    var selected = $('#node-gallery-rotate-dialog .selected').attr('class');
+    if (typeof selected != 'undefined') {
+      var degrees = selected.match(/ng3-rotate-([0-9]+)/);
+      self.link.parent().find(':input:radio[id$="'+degrees[1]+'"]').attr('checked', true).click();
+    }
+    self.dialog.dialog("close");
+  };
+
+  var _buttons = {};
+  _buttons[Drupal.t('Ok')] = buttonOk;
+  _buttons[Drupal.t('Cancel')] = function () {self.dialog.dialog("close");};
+  if ($('a.ng3-rotate-link', context).length > 0) {
+    this.dialog = $('<div id="node-gallery-rotate-dialog"></div>')
+      .dialog({
+        autoOpen: false,
+        modal: true,
+        resizable: true,
+        title: 'Rotate image',
+        width: 400,
+        height: 250,
+        minWidth: 400,
+        minHeight: 250,
+        buttons: _buttons
+      });
+  }
+  $('a.ng3-rotate-link', context).each(function () {
+    $(this).click(function () {
+      self.link = $(this);
+      self.dialog.html(Drupal.theme('nodeGalleryRotateDialog', $(this).attr('rel'))).dialog('open');
+      $('#node-gallery-rotate-dialog img').each( function () {
+        $(this).click(function () {
+          $(this).addClass('selected').siblings('.selected').removeClass('selected');
+        })
+      });
+      return false;
+    });
+  });
+  $(':input:radio[name^="images"][name$="\[rotate\]"]', context).each(function () {
+    if ($(this).val() == "0") {
+      $(this).attr('checked', 'checked');
+    }
+    $(this).click(function () {
+      $(this).parents('tr:first').find('img.imagecache-node-gallery-admin-thumbnail').removeClass().addClass('imagecache-node-gallery-admin-thumbnail ng3-rotate-'+$(this).val());
+    });
+  });
+};
Index: theme/theme.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/node_gallery/theme/theme.inc,v
retrieving revision 1.2.2.46
diff -u -p -r1.2.2.46 theme.inc
--- theme/theme.inc	22 Dec 2010 17:34:27 -0000	1.2.2.46
+++ theme/theme.inc	29 Dec 2010 10:55:54 -0000
@@ -11,9 +11,21 @@ function theme_node_gallery_manage_image
   drupal_add_js('misc/tableselect.js');
   // get fieldname to retrieve the filepath for the thumbnail without loading the node
   $relationship = node_gallery_get_relationship($form['#gallery']->type);
+  $enable_rotation = FALSE;
+  if ($relationship['settings']['manage_images_enable_rotation'] && (imageapi_default_toolkit() != 'imageapi_gd' || function_exists("imagerotate"))) {
+    $enable_rotation = TRUE;
+    if (module_exists('jquery_ui')) {
+      drupal_add_css(drupal_get_path('module', 'jquery_ui').'/jquery.ui/themes/default/ui.all.css');
+      jquery_ui_add(array('ui.dialog', 'ui.draggable', 'ui.resizable'));
+    }
+    drupal_add_js(drupal_get_path('module', 'node_gallery').'/js/ng_manage_images.js');
+  }
   $field_name = $relationship['imagefield_name'];
   $thumb_imagecache = $form['#thumb_imagecache'];
   $header = array(array('data' => t('Delete'), 'class' => 'select-all'), t('Preview'), t('Edit'), t('Cover'));
+  if ($enable_rotation) {
+    $header = array(array('data' => t('Delete'), 'class' => 'select-all'), t('Preview'), t('Edit'), t('Rotation'), t('Cover'));
+  }
   foreach (element_children($form['images']) as $nid) {
     $element = &$form['images'][$nid];
     $filepath = node_gallery_get_image_filepath($element['edit_form']['nid']['#value'], $field_name);
@@ -26,6 +38,9 @@ function theme_node_gallery_manage_image
       $edit .= drupal_render($element['gid']);
     }
     $row[] = $edit;
+    if ($enable_rotation) {
+      $row[] = drupal_render($element['rotate']);
+    }
     if ($form['is_cover']) {
       $row[] = drupal_render($form['is_cover'][$nid]);
     }
