Added initial support for notes (aka tags) on images, done completely (including initial load of metadata) with AJAX. They're not fixed at 100x100 like on Facebook either.
authorDan
Sat, 16 Feb 2008 23:07:31 -0500
changeset 18 c1c398349651
parent 15 ac7d3dedcc44
child 19 08bf8aa2f0ab
child 20 d8a281557365
Added initial support for notes (aka tags) on images, done completely (including initial load of metadata) with AJAX. They're not fixed at 100x100 like on Facebook either.
plugins/Gallery.php
plugins/gallery/browser.css
plugins/gallery/canvas.js
plugins/gallery/functions.php
plugins/gallery/nssetup.php
plugins/gallery/src/tag-image.xcf
plugins/gallery/tag-image.gif
plugins/gallery/tagging.js
plugins/gallery/viewimage.php
--- a/plugins/Gallery.php	Sat Nov 24 11:43:34 2007 -0500
+++ b/plugins/Gallery.php	Sat Feb 16 23:07:31 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	Sat Nov 24 11:43:34 2007 -0500
+++ b/plugins/gallery/browser.css	Sat Feb 16 23:07:31 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:07:31 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	Sat Nov 24 11:43:34 2007 -0500
+++ b/plugins/gallery/functions.php	Sat Feb 16 23:07:31 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	Sat Nov 24 11:43:34 2007 -0500
+++ b/plugins/gallery/nssetup.php	Sat Feb 16 23:07:31 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:07:31 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	Sat Nov 24 11:43:34 2007 -0500
+++ b/plugins/gallery/viewimage.php	Sat Feb 16 23:07:31 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']) )