Index: trac/attachment.py
===================================================================
--- trac/attachment.py	(revision 8466)
+++ trac/attachment.py	(working copy)
@@ -20,6 +20,7 @@
 import os
 import re
 import shutil
+import Image
 import time
 import unicodedata
 
@@ -116,6 +117,7 @@
         self.env = env
         self.parent_realm = self.resource.parent.realm
         self.parent_id = unicode(self.resource.parent.id)
+        self.first1000bytes = None
         if self.resource.id:
             self._fetch(self.resource.id, db)
         else:
@@ -125,6 +127,8 @@
             self.date = None
             self.author = None
             self.ipnr = None
+            self.mimetype = None
+            self.charset = None
 
     def _set_filename(self, val):
         self.resource.id = val
@@ -152,7 +156,48 @@
         self.date = datetime.fromtimestamp(time, utc)
         self.author = row[4]
         self.ipnr = row[5]
+        self._get_mimetype()
+        self._get_charset()
 
+    def _get_first_1000_bytes(self):
+	if not self.first1000bytes:
+	    fd = self.open()
+	    try:
+		str_data = fd.read(1000)
+        	fd.seek(0)
+    	    finally:
+        	fd.close()
+
+    def _get_charset(self):
+	self._get_first_1000_bytes()
+	mimeview = Mimeview(self.env)
+        self.charset = mimeview.get_charset(self.first1000bytes, self.mimetype)
+
+    def _get_mimetype(self):
+	self._get_first_1000_bytes()
+	mimeview = Mimeview(self.env)
+        self.mimetype = mimeview.get_mimetype(self.filename, self.first1000bytes)
+        if not self.mimetype:
+            self.mimetype = 'application/octet-stream'
+
+    def getThumbail(self, size = (160,160)):
+	image = Image.open(self.path)
+	thumbDir=os.path.join(self._get_dirPath(),'.thumb')
+	thumbFile=os.path.join(thumbDir,unicode_quote(self.filename))
+	if not os.path.isdir(thumbDir):
+	    os.mkdir(thumbDir)
+	# 200 x 150
+	if os.path.exists(thumbFile):
+	    return thumbFile
+	if image.size[0] < size[0] and image.size[0] < size[0]:
+	    shutil.copyfile(self.path,thumbFile)
+	    return thumbFile
+	image.thumbnail(size)
+	image.save(thumbFile)
+	image.close()
+    def _get_dirPath(self):
+	return os.path.normpath(os.path.join(self.env.path, 'attachments', self.parent_realm,
+                            unicode_quote(self.parent_id)))
     def _get_path(self):
         path = os.path.join(self.env.path, 'attachments', self.parent_realm,
                             unicode_quote(self.parent_id))
@@ -266,6 +311,8 @@
             attachment.date = datetime.fromtimestamp(time, utc)
             attachment.author = author
             attachment.ipnr = ipnr
+	    attachment._get_mimetype()
+    	    attachment._get_charset()
             yield attachment
 
     def delete_all(cls, env, parent_realm, parent_id, db):
@@ -349,12 +396,14 @@
     # IRequestHandler methods
 
     def match_request(self, req):
-        match = re.match(r'/(raw-)?attachment/([^/]+)(?:/(.*))?$',
+        match = re.match(r'/(raw-|thumb-)?attachment/([^/]+)(?:/(.*))?$',
                          req.path_info)
         if match:
             raw, realm, path = match.groups()
-            if raw:
+            if raw == 'raw-':
                 req.args['format'] = 'raw'
+            elif raw == 'thumb-':
+                req.args['format'] = 'thumb'
             req.args['realm'] = realm
             if path:
                 req.args['path'] = path
@@ -415,6 +464,7 @@
 
     def get_link_resolvers(self):
         yield ('raw-attachment', self._format_link)
+        yield ('thumb-attachment', self._format_link)
         yield ('attachment', self._format_link)
 
     # Public methods
@@ -520,6 +570,9 @@
         if format == 'raw':
             kwargs.pop('format')
             prefix = 'raw-attachment'
+        elif format == 'thumb':
+    	    kwargs.pop('format')
+    	    prefix = 'thumb-attachment'
         parent_href = unicode_unquote(get_resource_url(self.env,
                             resource.parent(version=None), Href('')))
         if not resource.id: 
@@ -544,14 +597,10 @@
 
     # Internal methods
 
-    def _do_save(self, req, attachment):
+    def _do_save_one(self, req, attachment, upload, description):
         req.perm(attachment.resource).require('ATTACHMENT_CREATE')
 
-        if 'cancel' in req.args:
-            req.redirect(get_resource_url(self.env, attachment.resource.parent,
-                                          req.href))
 
-        upload = req.args['attachment']
         if not hasattr(upload, 'filename') or not upload.filename:
             raise TracError(_('No file uploaded'))
         if hasattr(upload.file, 'fileno'):
@@ -579,7 +628,7 @@
             raise TracError(_('No file uploaded'))
         # Now the filename is known, update the attachment resource
         # attachment.filename = filename
-        attachment.description = req.args.get('description', '')
+        attachment.description = description
         attachment.author = get_reporter_id(req, 'author')
         attachment.ipnr = req.remote_addr
 
@@ -608,8 +657,31 @@
             attachment.filename = None
         attachment.insert(filename, upload.file, size)
 
-        req.redirect(get_resource_url(self.env, attachment.resource(id=None),
+    def _do_save_many(self,req, attachment):
+	curr_attach = 0
+	while curr_attach < int(req.args['multiple_attachments']):
+	    upload=req.args['attachment-%d' % curr_attach]
+	    if hasattr(upload, 'filename') and upload.filename:
+		attachment.filename = None
+		self._do_save_one(req, attachment, upload, req.args.get('description-%d' % curr_attach, ''))
+	    curr_attach=curr_attach+1
+
+    def _do_save(self, req, attachment):
+        req.perm(attachment.resource).require('ATTACHMENT_CREATE')
+
+        if 'cancel' in req.args:
+            req.redirect(get_resource_url(self.env, attachment.resource.parent,
+                                          req.href))
+
+	if 'multiple_attachments' in req.args:
+	    self._do_save_many(req, attachment)
+	    req.redirect(get_resource_url(self.env, attachment.resource(id=None),
                                       req.href))
+	else:
+	    self._do_save_one(req, attachment, req.args['attachment'], req.args.get('description', ''))
+	    # Redirect the user to list of attachments (must add a trailing '/')
+	    req.redirect(get_resource_url(self.env, attachment.resource(id=None),
+                                      req.href))
 
     def _do_delete(self, req, attachment):
         req.perm(attachment.resource).require('ATTACHMENT_DELETE')
@@ -655,20 +727,20 @@
                 'title': get_resource_name(self.env, attachment.resource),
                 'attachment': attachment}
 
+        mime_type = attachment.mimetype
         fd = attachment.open()
         try:
             mimeview = Mimeview(self.env)
 
-            # MIME type detection
-            str_data = fd.read(1000)
-            fd.seek(0)
-            
-            mime_type = mimeview.get_mimetype(attachment.filename, str_data)
-
             # Eventually send the file directly
             format = req.args.get('format')
-            if format in ('raw', 'txt'):
+            if format == 'thumb':
                 if not self.render_unsafe_content:
+                    req.send_header('Content-Disposition', 'attachment')
+        	if attachment.mimetype[0:5] in ('image'):
+        	    req.send_file(attachment.getThumbail(), mime_type)
+            elif format in ('raw', 'txt'):
+                if not self.render_unsafe_content:
                     # Force browser to download files instead of rendering
                     # them, since they might contain malicious code enabling 
                     # XSS attacks
@@ -678,6 +750,9 @@
                 elif not mime_type:
                     mime_type = 'application/octet-stream'
                 if 'charset=' not in mime_type:
+                    str_data = fd.read(1000)
+                    fd.seek(0)
+
                     charset = mimeview.get_charset(str_data, mime_type)
                     mime_type = mime_type + '; charset=' + charset
                 req.send_file(attachment.path, mime_type)
@@ -730,6 +805,8 @@
                 format = None
                 if ns.startswith('raw'):
                     format = 'raw'
+                elif ns.startswith('thumb'):
+                    format = 'thumb'
                 href = get_resource_url(self.env, attachment, formatter.href,
                                         format=format)
                 return tag.a(label, class_='attachment', href=href + params,
Index: trac/htdocs/js/trac.js
===================================================================
--- trac/htdocs/js/trac.js	(revision 8466)
+++ trac/htdocs/js/trac.js	(working copy)
@@ -71,5 +71,79 @@
   window.getAncestorByTagName = function(elem, tagName) {
     return $(elem).parents(tagName).get(0);
   }
+})(jQuery);
 
-})(jQuery);
\ No newline at end of file
+var ATTACHFILE_COUNTER=0;
+function manageMultipleAttachFields(_obj){
+    if (_obj.value == '') {
+        if($("#multiAttach_tbody").get(0).rows.length == 1) return;
+        $("#multiAttach_tbody").get(0).deleteRow($(_obj).attr("attachnum")*1);
+        ATTACHFILE_COUNTER=0;
+        $("#multiAttach_tbody tr").each(function(index, element){
+            $("input", $("td", element).get(0))
+                .attr("attachnum",ATTACHFILE_COUNTER)
+                .get(0).name = "attachment-"+ATTACHFILE_COUNTER;
+            $("input", $("td", element).get(1))
+                .get(0).name = "description-"+ATTACHFILE_COUNTER;
+            ATTACHFILE_COUNTER++;
+        });
+        $("#multiAttach_count").get(0).value = $("#multiAttach_count").val()*1-1;
+    } else {
+        if ($(_obj).attr("attachnum") != $("#multiAttach_count").val()-1) return;
+        var tr = $("#multiAttach_tbody").get(0).insertRow(-1);
+        var td = tr.insertCell(-1);
+        $('<input type="file" onchange="manageMultipleAttachFields(this)">')
+           .attr("attachnum",$("#multiAttach_count").val())
+           .appendTo(td)
+           .get(0).name = "attachment-"+$("#multiAttach_count").val();
+
+        td = tr.insertCell(-1);
+        $('<input type="text">')
+            .attr("size",60)
+            .appendTo(td)
+            .get(0).name = "description-"+$("#multiAttach_count").val();
+        $("#multiAttach_count").get(0).value = $("#multiAttach_count").val()*1+1;
+    }
+}
+
+function clearAuthenticationCache(page) {
+  // Default to a non-existing page (give error 500).
+  // An empty page is better, here.
+  if (!page) page = '.force_logout';
+  try{
+    var agt=navigator.userAgent.toLowerCase();
+    if (agt.indexOf("msie") != -1) {
+      // IE clear HTTP Authentication
+      document.execCommand("ClearAuthenticationCache");
+    }
+    else {
+      // Let's create an xmlhttp object
+      var xmlhttp = createXMLObject();
+      // Let's prepare invalid credentials
+      xmlhttp.open("GET", page, true, "logout", "logout");
+      // Let's send the request to the server
+      xmlhttp.send("");
+      // Let's abort the request
+      xmlhttp.abort();
+    }
+  } catch(e) {
+    // There was an error
+    return;
+  }
+}
+    
+function createXMLObject() {
+  try {
+    if (window.XMLHttpRequest) {
+      xmlhttp = new XMLHttpRequest();
+    }
+    // code for IE
+    else if (window.ActiveXObject) {
+      xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
+    }
+  } catch (e) {
+    xmlhttp=false
+  }
+  return xmlhttp;
+}
+
Index: trac/templates/macros.html
===================================================================
--- trac/templates/macros.html	(revision 8466)
+++ trac/templates/macros.html	(working copy)
@@ -189,16 +189,45 @@
           </ul>
         </py:when>
         <py:when test="not compact">
-          <h2>Attachments</h2>
           <div py:if="alist.attachments or alist.can_create" id="attachments">
+          <h2>Attachments (Files)</h2>
             <dl py:if="alist.attachments" class="attachments">
-              <py:for each="attachment in alist.attachments">
+              <py:for each="attachment in [f for f in alist.attachments if f.mimetype[0:5] not in ('image') ]">
                 <dt>${show_one_attachment(attachment)}</dt>
                 <dd py:if="attachment.description">
                   ${wiki_to_oneliner(context, attachment.description)}
                 </dd>
               </py:for>
             </dl>
+          <h2>Attachments  (Images)</h2>
+             <py:with vars="attachs = [f for f in alist.attachments if f.mimetype[0:5] in ('image') ]; fullrow = len(attachs)">
+                <table align="center">
+                <tr py:for="row in group(attachs, 3)">
+                <py:for each="oneattach in row">
+                <td py:if="not oneattach" width="210">
+                &nbsp;
+                </td>
+                <td py:if="oneattach" align="center" style="border: 1px solid #f0f0f0; text-align: center;">
+                <div style="width: 200px; height: 200px;">
+                    <a href="${url_of(oneattach.resource)}">
+                        <img src="${url_of(oneattach.resource,format='thumb')}"
+                        style="cursor: pointer;"
+                        border="0"
+                        id="img_${oneattach.filename}"
+                        />
+                    </a>
+                </div>
+                <div style="background-color: #f0f0f0;">
+                    <py:with vars="attach_size = round(float(oneattach.size)/float(1024),3)">
+                    <small>${oneattach.filename} (${attach_size} kb)<br />
+                    <i>${oneattach.description}</i>
+                    </small>
+                    </py:with>
+                </div>
+                </td>
+                </py:for>
+                </tr></table>
+             </py:with>
             ${attach_file_form(alist, add_button_title)}
           </div>
         </py:when>
Index: trac/templates/attachment.html
===================================================================
--- trac/templates/attachment.html	(revision 8466)
+++ trac/templates/attachment.html	(working copy)
@@ -12,10 +12,7 @@
 
   <body py:with="parent = attachments and attachments.parent or
                           attachment.resource.parent">
-    <div py:choose="mode" id="content" class="attachment">
-      <py:when test="'new'">
-        <h1>Add Attachment to <a href="${url_of(parent)}">${name_of(parent)}</a></h1>
-        <form id="attachment" method="post" enctype="multipart/form-data" action="">
+    <py:def function="attach_oneFile()">
           <div class="field">
             <label>File<py:if test="max_size >= 0"> (size limit
                   ${pretty_size(max_size, format='%d')})</py:if>:<br />
@@ -41,6 +38,44 @@
             </div>
             <br />
           </fieldset>
+    
+    </py:def>
+    <py:def function="attach_multiFile()">
+	 <table><thead>
+          <tr><th align="left">File</th><th align="left">Description</th></tr>
+	 </thead>
+	<tbody id="multiAttach_tbody">
+    	<tr>
+    	    <td><input type="file" attachnum="0" name="attachment-0" onchange="manageMultipleAttachFields(this);"/></td>
+    	    <td><input type="text" name="description-0" size="60" /></td>
+    	</tr>
+    	</tbody></table>
+        <input type="hidden" id="multiAttach_count" name="multiple_attachments" value="1" />
+
+          <fieldset>
+            <legend>Attachment Info</legend>
+            <py:if test="authname == 'anonymous'">
+              <div class="field">
+                <label>Your email or username:<br />
+                  <input type="text" name="author" size="30" value="$author" />
+                </label>
+              </div>
+              </py:if>
+            <div class="options">
+              <label><input type="checkbox" name="replace" />
+                Replace existing attachments of the same name</label>
+            </div>
+            <br />
+          </fieldset>
+    
+    </py:def>
+
+    <div py:choose="mode" id="content" class="attachment">
+      <py:when test="'new'">
+        <h1>Add Attachment to <a href="${url_of(parent)}">${name_of(parent)}</a></h1>
+        <form id="attachment" method="post" enctype="multipart/form-data" action="">
+        
+        ${attach_multiFile()}
           <div class="buttons">
             <input type="hidden" name="action" value="new" />
             <input type="hidden" name="realm" value="$parent.realm" />

