Merging in changes (again?) from Scribus.
authorDan
Sat, 16 Feb 2008 23:10:17 -0500
changeset 20 d8a281557365
parent 17 ecb96b051d08 (current diff)
parent 18 c1c398349651 (diff)
child 21 0475c9ed1386
Merging in changes (again?) from Scribus.
--- a/plugins/Gallery.php	Wed Dec 05 16:53:26 2007 -0500
+++ b/plugins/Gallery.php	Sat Feb 16 23:10:17 2008 -0500
@@ -10,7 +10,7 @@
 
 global $db, $session, $paths, $template, $plugins; // Common objects
 
-define('GALLERY_VERSION', '0.1b1');
+define('GALLERY_VERSION', '0.1b2');
 
 $magick_path = getConfig('imagemagick_path');
 if ( !file_exists($magick_path) || !is_executable($magick_path) )
@@ -32,6 +32,7 @@
                         img_filename varchar(255) NOT NULL,
                         img_time_upload int(12) NOT NULL DEFAULT 0,
                         img_time_mod int(12) NOT NULL DEFAULT 0,
+                        img_tags longtext DEFAULT NULL,
                         PRIMARY KEY ( img_id )
                       );');
   
@@ -50,6 +51,13 @@
   
   setConfig('gallery_version', GALLERY_VERSION);
 }
+if ( getConfig('gallery_version') == '0.1b1' )
+{
+  $q = $db->sql_query('ALTER TABLE ' . table_prefix . 'gallery ADD COLUMN img_tags longtext DEFAULT NULL');
+  if ( !$q )
+    $db->_die();
+  setConfig('gallery_version', '0.1b2');
+}
 
 require( ENANO_ROOT . '/plugins/gallery/functions.php' );
 require( ENANO_ROOT . '/plugins/gallery/nssetup.php' );
--- a/plugins/gallery/browser.css	Wed Dec 05 16:53:26 2007 -0500
+++ b/plugins/gallery/browser.css	Sat Feb 16 23:10:17 2008 -0500
@@ -46,4 +46,23 @@
   vertical-align: middle;
 }
 
+div.snapr_tag_entry {
+  border: 1px solid #202020;
+  background-color: #FFFFEB;
+  color: #101010;
+  padding: 4px;
+  -moz-border-radius: 8px;
+  text-align: left;
+}
 
+div.snapr_tag {
+  border: 1px solid #202020;
+  background-color: #EBEBFF;
+  color: #101010;
+  padding: 4px;
+  text-align: left;
+  min-width: 100px;
+  font-family: arial, helvetica, sans-serif;
+  font-size: 8pt;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/gallery/canvas.js	Sat Feb 16 23:10:17 2008 -0500
@@ -0,0 +1,177 @@
+var canvas_mousemove_temp;
+var canvas_keyup_temp;
+var CANVAS_KEY_ESC = 27;
+
+function canvas_click(obj)
+{
+  var click_x = mouseX - $(obj).Left();
+  var click_y = mouseY - $(obj).Top() + getScrollOffset();
+  
+  if ( obj.canvas_in_draw )
+  {
+    canvas_close_draw(obj, click_x, click_y);
+  }
+  else
+  {
+    canvas_open_draw(obj, click_x, click_y);
+  }
+}
+
+function canvas_open_draw(obj, x, y)
+{
+  obj.canvas_box_obj = canvas_create_box(obj, x, y, 1, 1);
+  obj.canvas_in_draw = true;
+  obj.onclick = function(e)
+  {
+    canvas_click(this);
+    var onclose = this.getAttribute('canvas:oncomplete');
+    if ( onclose )
+    {
+      eval(onclose);
+    }
+  }
+  canvas_replace_mousemove(obj);
+}
+
+function canvas_replace_mousemove(obj)
+{
+  canvas_mousemove_temp = document.onmousemove;
+  canvas_mousemove_temp.box_obj = obj;
+  canvas_keyup_temp = document.onkeyup;
+  document.onmousemove = function(e)
+  {
+    canvas_mousemove_temp(e);
+    canvas_redraw_box(canvas_mousemove_temp.box_obj);
+  }
+  document.onkeyup = function(e)
+  {
+    if ( typeof(canvas_keyup_temp) == 'function' )
+      canvas_keyup_temp(e);
+    
+    if ( e.keyCode == CANVAS_KEY_ESC )
+      canvas_cancel_draw(canvas_mousemove_temp.box_obj);
+  }
+}
+
+function canvas_restore_mousemove()
+{
+  document.onmousemove = canvas_mousemove_temp;
+  document.onkeyup = canvas_keyup_temp;
+}
+
+function canvas_create_box(obj, x, y, width, height)
+{
+  var inner_width = width - 2;
+  var inner_height = height - 2;
+  var top = $(obj).Top() + y;
+  var left = $(obj).Left() + x;
+  
+  // draw outer box
+  var div_outer = document.createElement('div');
+  div_outer.className = 'canvasbox';
+  div_outer.style.border = '1px solid #000000';
+  div_outer.style.position = 'absolute';
+  div_outer.style.width = String(width) + 'px';
+  div_outer.style.height = String(height) + 'px';
+  div_outer.style.top = String(top) + 'px';
+  div_outer.style.left = String(left) + 'px';
+  
+  div_outer.rootY = y;
+  div_outer.rootX = x;
+  
+  var div_inner = document.createElement('div');
+  div_inner.style.border = '1px solid #FFFFFF';
+  if ( IE )
+  {
+    div_inner.style.width = '1px';
+    div_inner.style.height = '1px';
+  }
+  else
+  {
+    div_inner.style.width = String(inner_width) + 'px';
+    div_inner.style.height = String(inner_height) + 'px';
+  }
+  
+  div_outer.appendChild(div_inner);
+  
+  obj.appendChild(div_outer);
+  return div_outer;
+}
+
+function canvas_redraw_box(obj)
+{
+  if ( !obj.canvas_box_obj )
+    return false;
+  var rel_x = mouseX - $(obj).Left();
+  var rel_y = mouseY - $(obj).Top() + getScrollOffset();
+  var new_width = rel_x - obj.canvas_box_obj.rootX;
+  var new_height = rel_y - obj.canvas_box_obj.rootY;
+  var rootX = obj.canvas_box_obj.rootX;
+  var rootY = obj.canvas_box_obj.rootY;
+  // Limit dimensions to width - origin_x and height - origin_y
+  if ( new_width + rootX > $(obj).Width() )
+    new_width = $(obj).Width() - rootX;
+  if ( new_height + rootY > $(obj).Height() )
+    new_height = $(obj).Height() - rootY;
+  // If going to the top or left of the origin, avoid negative width/height by moving the box
+  if ( new_width < 1 )
+  {
+    new_width = rootX - rel_x;
+    obj.canvas_box_obj.style.left = String(mouseX + 2) + 'px';
+  }
+  if ( new_height < 1 )
+  {
+    new_height = rootY - rel_y;
+    obj.canvas_box_obj.style.top = String(mouseY + getScrollOffset() + 2) + 'px';
+  }
+  obj.canvas_box_obj.style.width = String(new_width) + 'px';
+  obj.canvas_box_obj.style.height = String(new_height) + 'px';
+  new_width = new_width - 2;
+  new_height = new_height - 2;
+  if ( IE )
+  {
+    var nw = new_width;
+    var nh = new_height;
+    obj.canvas_box_obj.firstChild.style.width = String(nw) + 'px';
+    obj.canvas_box_obj.firstChild.style.height = String(nh) + 'px';
+  }
+  else
+  {
+    obj.canvas_box_obj.firstChild.style.width = String(new_width) + 'px';
+    obj.canvas_box_obj.firstChild.style.height = String(new_height) + 'px';
+  }
+}
+
+function canvas_close_draw(obj, x, y)
+{
+  canvas_restore_mousemove();
+  obj.canvas_in_draw = false;
+  obj.canvas = {
+    top: $(obj.canvas_box_obj).Top() - $(obj).Top(),
+    left: $(obj.canvas_box_obj).Left() - $(obj).Left(),
+    width: $(obj.canvas_box_obj).Width(),
+    height: $(obj.canvas_box_obj).Height()
+  }
+  obj.onclick = function(e)
+  {
+    canvas_click(this);
+  }
+}
+
+function canvas_cancel_draw(obj)
+{
+  canvas_restore_mousemove();
+  obj.canvas_in_draw = false;
+  obj.removeChild(obj.canvas_box_obj);
+  obj.canvas_box_obj = null;
+  obj.onclick = function(e)
+  {
+    canvas_click(this);
+  }
+  var ga = obj.getAttribute('canvas:oncancel');
+  if ( ga )
+  {
+    eval(ga);
+  }
+}
+
--- a/plugins/gallery/functions.php	Wed Dec 05 16:53:26 2007 -0500
+++ b/plugins/gallery/functions.php	Sat Feb 16 23:10:17 2008 -0500
@@ -314,4 +314,66 @@
   return $entries;
 }
 
+/**
+ * Wrapper for JSON decoding that works on Enano 1.0.x and 1.1.x
+ * @param string JSON datastream...
+ * @return mixed
+ */
+
+function snapr_json_decode($data)
+{
+  if ( defined('ENANO_ATLEAST_1_1') )
+  {
+    try
+    {
+      $decoded = enano_json_decode($data);
+    }
+    catch ( Exception $e )
+    {
+      $response = array(
+        'mode' => 'error',
+        'error' => 'Exception in JSON parser.'
+      );
+      die(enano_json_encode($response));
+    }
+  }
+  else
+  {
+    $json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE);
+    $decoded = $json->decode($data);
+  }
+  return ( isset($decoded) ) ? $decoded : false;
+}
+
+/**
+ * Wrapper for JSON encoding that works on Enano 1.0.x and 1.1.x
+ * @param mixed Data to encode
+ * @return string
+ */
+
+function snapr_json_encode($data)
+{
+  if ( defined('ENANO_ATLEAST_1_1') )
+  {
+    try
+    {
+      $encoded = enano_json_encode($data);
+    }
+    catch ( Exception $e )
+    {
+      $response = array(
+        'mode' => 'error',
+        'error' => 'Exception in JSON encoder.'
+      );
+      die(enano_json_encode($response));
+    }
+  }
+  else
+  {
+    $json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE);
+    $encoded = $json->encode($data);
+  }
+  return ( isset($encoded) ) ? $encoded : false;
+}
+
 ?>
--- a/plugins/gallery/nssetup.php	Wed Dec 05 16:53:26 2007 -0500
+++ b/plugins/gallery/nssetup.php	Sat Feb 16 23:10:17 2008 -0500
@@ -21,6 +21,7 @@
   $paths->create_namespace('Gallery', 'Image:');
   
   $session->register_acl_type('gal_full_res', AUTH_ALLOW, 'View image at full resolution', array('read'), 'Gallery');
+  $session->register_acl_type('snapr_add_tag', AUTH_DISALLOW, 'Add image tags (separate from adding normal tags)', array('read'), 'Gallery');
   
   $session->acl_extend_scope('read',                   'Gallery', $paths);
   $session->acl_extend_scope('post_comments',          'Gallery', $paths);
Binary file plugins/gallery/src/tag-image.xcf has changed
Binary file plugins/gallery/tag-image.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/gallery/tagging.js	Sat Feb 16 23:10:17 2008 -0500
@@ -0,0 +1,240 @@
+function snapr_add_tag()
+{
+  var image = document.getElementById('snapr_preview_img');
+  image.parentNode.onclick = function(e)
+  {
+    canvas_click(this);
+  }
+  image.parentNode.setAttribute('canvas:oncomplete', 'snapr_process_canvas_add(this);');
+  image.parentNode.setAttribute('canvas:oncancel', 'obj.onclick = null;');
+}
+
+function snapr_process_canvas_add(obj)
+{
+  obj.onclick = null;
+  var abs_x = $(obj).Left() + obj.canvas.left;
+  var abs_y = $(obj).Top()  + obj.canvas.top;
+  var height = obj.canvas.height + 2;
+  
+  var entry_div = document.createElement('div');
+  entry_div.className = 'snapr_tag_entry';
+  entry_div.style.position = 'absolute';
+  entry_div.style.top = String(abs_y + height) + 'px';
+  entry_div.style.left = String(abs_x)+ 'px';
+  
+  entry_div.appendChild(document.createTextNode('Enter a tag:'));
+  entry_div.appendChild(document.createElement('br'));
+  
+  var ta = document.createElement('textarea');
+  ta.rows = '7';
+  ta.cols = '30';
+  entry_div.appendChild(ta);
+  
+  entry_div.appendChild(document.createElement('br'));
+  
+  var a_add = document.createElement('a');
+  a_add.href = '#';
+  a_add.onclick = function()
+  {
+    snapr_finalize_canvas_add(this.parentNode, this.parentNode.parentNode.canvas, this.previousSibling.previousSibling.value);
+    return false;
+  }
+  a_add.appendChild(document.createTextNode('Add tag'));
+  entry_div.appendChild(a_add);
+  
+  entry_div.appendChild(document.createTextNode(' | '));
+  
+  var a_cancel = document.createElement('a');
+  a_cancel.href = '#';
+  a_cancel.onclick = function()
+  {
+    snapr_finalize_canvas_cancel(this.parentNode);
+    return false;
+  }
+  a_cancel.appendChild(document.createTextNode('Cancel'));
+  entry_div.appendChild(a_cancel);
+  
+  obj.appendChild(entry_div);
+  ta.focus();
+}
+
+function snapr_finalize_canvas_add(obj, canvas_data, tag)
+{
+  // add the new box
+  var id = obj.parentNode.getAttribute('snapr:imgid');
+  if ( !id )
+    return false;
+  
+  // destroy form, etc.
+  var parent = obj.parentNode;
+  parent.removeChild(parent.canvas_box_obj);
+  parent.removeChild(obj);
+  
+  var canvas_json = toJSONString(canvas_data);
+  ajaxPost(makeUrlNS('Gallery', id), 'ajax=true&act=add_tag&tag=' + escape(tag) + '&canvas_params=' + escape(canvas_json), snapr_process_ajax_tag_packet);
+}
+
+function snapr_finalize_canvas_cancel(obj)
+{
+  var parent = obj.parentNode;
+  parent.removeChild(parent.canvas_box_obj);
+  parent.removeChild(obj);
+}
+
+function snapr_draw_note(obj, tag, canvas_data, note_id, initial_hide, auth_delete)
+{
+  var newbox = canvas_create_box(obj, canvas_data.left, canvas_data.top, canvas_data.width, canvas_data.height);
+  newbox.tag_id = note_id;
+  obj.onmouseover = function()
+  {
+    var boxen = this.getElementsByTagName('div');
+    for ( var i = 0; i < boxen.length; i++ )
+      if ( boxen[i].className == 'canvasbox' )
+        boxen[i].style.display = 'block';
+  }
+  obj.onmouseout = function()
+  {
+    var boxen = this.getElementsByTagName('div');
+    for ( var i = 0; i < boxen.length; i++ )
+      if ( boxen[i].className == 'canvasbox' )
+        boxen[i].style.display = 'none';
+  }
+  newbox.onmouseover = function()
+  {
+    this.style.borderColor = '#FFFF00';
+    this.firstChild.style.borderColor = '#000000';
+    snapr_display_note(this.noteObj);
+  }
+  newbox.onmouseout = function()
+  {
+    this.style.borderColor = '#000000';
+    this.firstChild.style.borderColor = '#FFFFFF';
+    snapr_hide_note(this.noteObj);
+  }
+  if ( auth_delete )
+  {
+    var p = document.createElement('p');
+    p.style.cssFloat = 'right';
+    p.style.styleFloat = 'right';
+    p.style.fontWeight = 'bold';
+    p.style.margin = '5px';
+    var a_del = document.createElement('a');
+    a_del.style.color = '#FF0000';
+    a_del.href = '#';
+    a_del.onclick = function()
+    {
+      snapr_nuke_tag(this.parentNode.parentNode.parentNode);
+      return false;
+    }
+    a_del.appendChild(document.createTextNode('[X]'));
+    p.appendChild(a_del);
+    newbox.firstChild.appendChild(p);
+  }
+  var abs_x = $(newbox).Left();
+  var abs_y = $(newbox).Top() + $(newbox).Height() + 2;
+  var noteObj = document.createElement('div');
+  newbox.noteObj = noteObj;
+  noteObj.className = 'snapr_tag';
+  noteObj.style.display = 'none';
+  noteObj.style.position = 'absolute';
+  noteObj.style.top = abs_y + 'px';
+  noteObj.style.left = abs_x + 'px';
+  var re = new RegExp(unescape('%0A'), 'g');
+  noteObj.innerHTML = tag.replace(re, "<br />\n");
+  obj.appendChild(noteObj);
+  if ( initial_hide )
+    newbox.style.display = 'none';
+}
+
+function snapr_display_note(note)
+{
+  //domObjChangeOpac(0, note);
+  note.style.display = 'block';
+  //domOpacity(note, 0, 100, 500);
+}
+
+function snapr_hide_note(note)
+{
+  //domOpacity(note, 100, 0, 500);
+  //setTimeout(function()
+  //  {
+      note.style.display = 'none';
+  //  }, 600);
+}
+
+function snapr_nuke_tag(obj)
+{
+  // add the new box
+  var parent_obj = document.getElementById('snapr_preview_img').parentNode;
+  var id = parent_obj.getAttribute('snapr:imgid');
+  if ( !id )
+    return false;
+  ajaxPost(makeUrlNS('Gallery', id), 'ajax=true&act=del_tag&tag_id=' + obj.tag_id, snapr_process_ajax_tag_packet);
+}
+
+function snapr_process_ajax_tag_packet()
+{
+  if ( ajax.readyState == 4 && ajax.status == 200 )
+  {
+    var response = String(ajax.responseText + '');
+    if ( response.substr(0, 1) != '[' && response.substr(0, 1) != '{' )
+    {
+      handle_invalid_json(response);
+      return false;
+    }
+    response = parseJSON(response);
+    if ( response.mode )
+    {
+      if ( response.mode == 'error' )
+      {
+        alert(response.error);
+        return false;
+      }
+    }
+    var parent_obj = document.getElementById('snapr_preview_img').parentNode;
+    for ( var i = 0; i < response.length; i++ )
+    {
+      var packet = response[i];
+      switch(packet.mode)
+      {
+        case 'add':
+          snapr_draw_note(parent_obj, packet.tag, packet.canvas_data, packet.note_id, packet.initial_hide, packet.auth_delete);
+          break;
+        case 'remove':
+          // Server requested to remove a tag
+          var divs = parent_obj.getElementsByTagName('div');
+          for ( var i = 0; i < divs.length; i++ )
+          {
+            var box = divs[i];
+            if ( box.className == 'canvasbox' )
+            {
+              if ( box.tag_id == packet.note_id )
+              {
+                // You. We have orders to shoot. Stand in front of wall.
+                var sibling = box.nextSibling;
+                var parent = box.parentNode;
+                // BLAM.
+                parent.removeChild(sibling);
+                parent.removeChild(box);
+                break;
+              }
+            }
+          }
+          break;
+      }
+    }
+  }
+}
+
+var snapr_tags_onload = function()
+{
+  // add the new box
+  var parent_obj = document.getElementById('snapr_preview_img').parentNode;
+  var id = parent_obj.getAttribute('snapr:imgid');
+  if ( !id )
+    return false;
+  ajaxPost(makeUrlNS('Gallery', id), 'ajax=true&act=get_tags', snapr_process_ajax_tag_packet);
+}
+
+addOnloadHook(snapr_tags_onload);
+
--- a/plugins/gallery/viewimage.php	Wed Dec 05 16:53:26 2007 -0500
+++ b/plugins/gallery/viewimage.php	Sat Feb 16 23:10:17 2008 -0500
@@ -36,7 +36,7 @@
     $img_id = intval($page->page_id);
     if ( !$img_id )
       return false;
-    $q = $db->sql_query('SELECT img_id, img_title, img_desc, print_sizes, img_time_upload, img_time_mod, img_filename, folder_parent FROM '.table_prefix.'gallery WHERE img_id=' . $img_id . ';');
+    $q = $db->sql_query('SELECT img_id, img_title, img_desc, print_sizes, img_time_upload, img_time_mod, img_filename, folder_parent, img_tags FROM '.table_prefix.'gallery WHERE img_id=' . $img_id . ';');
     if ( !$q )
       $db->_die();
   }
@@ -61,7 +61,7 @@
     
     $folders = array_reverse($folders);
     // This is one of the best MySQL tricks on the market. We're going to reverse-travel a folder path using LEFT JOIN and the incredible power of metacoded SQL
-    $sql = 'SELECT g0.img_id, g0.img_title, g0.img_desc, g0.print_sizes, g0.img_time_upload, g0.img_time_mod, g0.img_filename, g0.folder_parent FROM '.table_prefix.'gallery AS g0';
+    $sql = 'SELECT g0.img_id, g0.img_title, g0.img_desc, g0.print_sizes, g0.img_time_upload, g0.img_time_mod, g0.img_filename, g0.folder_parent, g0.img_tags FROM '.table_prefix.'gallery AS g0';
     $where = "\n  " . 'WHERE g0.img_title=\'' . $db->escape($folders[0]) . '\'';
     foreach ( $folders as $i => $folder )
     {
@@ -166,11 +166,102 @@
   
   $db->free_result();
   
+  $perms = $session->fetch_page_acl(strval($img_id), 'Gallery');
+  
+  if ( isset($_POST['ajax']) && @$_POST['ajax'] === 'true' && isset($_POST['act']) )
+  {
+    $mode =& $_POST['act'];
+    $response = array();
+    switch($mode)
+    {
+      case 'add_tag':
+        if ( !$perms->get_permissions('snapr_add_tag') )
+        {
+          die(snapr_json_encode(array(
+              'mode' => 'error',
+              'error' => 'You don\'t have permission to add tags.'
+            )));
+        }
+        if ( empty($row['img_tags']) )
+        {
+          $row['img_tags'] = '[]';
+        }
+        $row['img_tags'] = snapr_json_decode($row['img_tags']);
+        
+        $canvas_data = snapr_json_decode($_POST['canvas_params']);
+        $tag_data = array(
+            'tag' => sanitize_html($_POST['tag']),
+            'canvas_data' => $canvas_data
+          );
+        $row['img_tags'][] = $tag_data;
+        $tag_data['note_id'] = count($row['img_tags']) - 1;
+        $tag_data['mode'] = 'add';
+        $tag_data['initial_hide'] = false;
+        $tag_data['auth_delete'] = true;
+        
+        $row['img_tags'] = snapr_json_encode($row['img_tags']);
+        $row['img_tags'] = $db->escape($row['img_tags']);
+        $q = $db->sql_query('UPDATE ' . table_prefix . "gallery SET img_tags = '{$row['img_tags']}' WHERE img_id = $img_id;");
+        if ( !$q )
+          $db->die_json();
+        
+        $response[] = $tag_data;
+        break;
+      case 'del_tag':
+        if ( !$perms->get_permissions('snapr_add_tag') )
+        {
+          die(snapr_json_encode(array(
+              'mode' => 'error',
+              'error' => 'You don\'t have permission to add tags.'
+            )));
+        }
+        if ( empty($row['img_tags']) )
+        {
+          $row['img_tags'] = '[]';
+        }
+        $row['img_tags'] = snapr_json_decode($row['img_tags']);
+        
+        $tag_id = intval(@$_POST['tag_id']);
+        if ( isset($row['img_tags'][$tag_id]) )
+          unset($row['img_tags'][$tag_id]);
+        
+        $row['img_tags'] = snapr_json_encode($row['img_tags']);
+        $row['img_tags'] = $db->escape($row['img_tags']);
+        $q = $db->sql_query('UPDATE ' . table_prefix . "gallery SET img_tags = '{$row['img_tags']}' WHERE img_id = $img_id;");
+        if ( !$q )
+          $db->die_json();
+        
+        $response[] = array(
+            'mode' => 'remove',
+            'note_id' => $tag_id
+          );
+        break;
+      case 'get_tags':
+        $response = snapr_json_decode($row['img_tags']);
+        foreach ( $response as $key => $_ )
+        {
+          unset($_);
+          $tag =& $response[$key];
+          $tag['note_id'] = $key;
+          $tag['mode'] = 'add';
+          $tag['initial_hide'] = true;
+          $tag['auth_delete'] = $perms->get_permissions('snapr_add_tag');
+        }
+        unset($tag);
+        break;
+    }
+    echo snapr_json_encode($response);
+    return true;
+  }
+  
+  $have_notes = ( empty($row['img_tags']) ) ? false : ( count(snapr_json_decode($row['img_tags'])) > 0 );
+  
+  $template->add_header('<script type="text/javascript" src="' . scriptPath . '/plugins/gallery/canvas.js"></script>');
+  $template->add_header('<script type="text/javascript" src="' . scriptPath . '/plugins/gallery/tagging.js"></script>');
+  
   $template->tpl_strings['PAGE_NAME'] = 'Gallery image: ' . htmlspecialchars($row['img_title']);
   $title_spacey = strtolower(htmlspecialchars($row['img_title']));
   
-  $perms = $session->fetch_page_acl(strval($img_id), 'Gallery');
-  
   $template->header();
   
   $img_id = intval($img_id);
@@ -198,17 +289,7 @@
   $img_url  = makeUrlNS('Special', 'GalleryFetcher/preview/' . $img_id);
   $img_href = makeUrlNS('Special', 'GalleryFetcher/full/' . $img_id);
   
-  if ( $perms->get_permissions('gal_full_res') )
-  {
-    echo '<a href="' . $img_href . '" title="Click to view this image at full resolution, right click to save image" onclick="window.open(this.href, \'\', \'toolbar=no,address=no,menus=no,status=no,scrollbars=yes\'); return false;">';
-  }
-  
-  echo '<img alt="Image preview (640px max width)" src="' . $img_url . '" style="border-width: 0; margin-bottom: 5px; display: block;" />';
-  
-  if ( $perms->get_permissions('gal_full_res') )
-  {
-    echo '</a>';
-  }
+  echo '<div snapr:imgid="' . $img_id . '"><img alt="Image preview (640px max width)" src="' . $img_url . '" id="snapr_preview_img" style="border-width: 0; margin-bottom: 5px; display: block;" /></div>';
   
   echo '<table border="0" width="100%"><tr><td style="text-align: left; width: 24px;">';
   
@@ -235,12 +316,32 @@
   
   echo '</td></tr>';
   echo '<tr><td colspan="3">' . "image $folder_this of $folder_total" . '</td></tr>';
+  if ( $perms->get_permissions('gal_full_res') || $have_notes )
+  {
+    echo '<tr><td colspan="3"><small>';
+    
+    if ( $perms->get_permissions('gal_full_res') )
+      echo "<a href=\"$img_href\" onclick=\"window.open(this.href, '', 'toolbar=no,address=no,menus=no,status=no,scrollbars=yes'); return false;\">View in original resolution</a>";
+    
+    if ( $perms->get_permissions('gal_full_res') && $have_notes )
+      echo ' :: ';
+    
+    if ( $have_notes )
+      echo 'Mouse over photo to view tags';
+    
+    echo '</small></td></tr>';
+  }
   echo '</table>';
   echo '</div>';
   
-  if ( $session->user_level >= USER_LEVEL_ADMIN )
+  if ( $session->user_level >= USER_LEVEL_ADMIN || $perms->get_permissions('snapr_add_tag') )
   {
-    echo '<div style="float: right;">[ <a href="' . makeUrlNS('Special', 'GalleryUpload', 'edit_img=' . $img_id, true) . '">edit image</a> ]</div>';
+    echo '<div style="float: right;">';
+    if ( $session->user_level >= USER_LEVEL_ADMIN )
+      echo '[ <a href="' . makeUrlNS('Special', 'GalleryUpload', 'edit_img=' . $img_id, true) . '">edit image</a> ] ';
+    if ( $perms->get_permissions('snapr_add_tag') )
+      echo '[ <a href="#" onclick="snapr_add_tag(); return false;"><img alt=" " src="' . scriptPath . '/plugins/gallery/tag-image.gif" style="border-width: 0;" /> add a tag</a> ] ';
+    echo '</div>';
   }
   
   if ( !empty($row['img_desc']) )