changeset 840:a7e157d258e8 jquery

merge
author hertzhaft
date Tue, 01 Mar 2011 22:00:50 +0100
parents 9fa078dcb493 (current diff) ea6c8f92c929 (diff)
children 73dd59b48c5e dad6a171bc52
files servlet/src/digilib/auth/HashTree.java servlet/src/digilib/image/ImageSize.java servlet/src/digilib/io/DigilibInfoReader.java servlet/src/digilib/io/DocuDirent.java servlet/src/digilib/io/ImageFileset.java servlet/src/digilib/io/MetadataMap.java servlet/src/digilib/io/XMLListLoader.java servlet/src/digilib/io/XMLMetaLoader.java
diffstat 57 files changed, 3861 insertions(+), 2221 deletions(-) [+]
line wrap: on
line diff
--- a/client/digitallibrary/ImgInfo-json.jsp	Tue Mar 01 17:12:25 2011 +0100
+++ b/client/digitallibrary/ImgInfo-json.jsp	Tue Mar 01 22:00:50 2011 +0100
@@ -1,6 +1,6 @@
 <%@page language="java" 
-	import="digilib.io.FileOps, digilib.io.ImageFileset, digilib.io.ImageFile, 
-	digilib.image.ImageSize, digilib.servlet.DigilibConfiguration"
+	import="digilib.io.FileOps, digilib.io.ImageFileSet, digilib.io.ImageFile, 
+	digilib.util.ImageSize, digilib.servlet.DigilibConfiguration"
 	contentType="application/json"%><%!
 // create DocumentBean instance for all JSP requests
 digilib.servlet.DocumentBean docBean = new digilib.servlet.DocumentBean();
@@ -23,14 +23,11 @@
 digilib.io.DocuDirCache dirCache = (digilib.io.DocuDirCache) dlConfig.getValue("servlet.dir.cache");
 // get file
 FileOps.FileClass fc = FileOps.FileClass.IMAGE;
-ImageFileset imgFile = (ImageFileset) dirCache.getFile(dlRequest.getFilePath(), dlRequest.getAsInt("pn"), fc);
+ImageFileSet imgFile = (ImageFileSet) dirCache.getFile(dlRequest.getFilePath(), dlRequest.getAsInt("pn"), fc);
 
 %>{ <% 
     if (imgFile != null) {
-		ImageFile img = imgFile.getBiggest();
-		if (!img.isChecked()) {
-			DigilibConfiguration.docuImageIdentify(img);
-		}
+		ImageFile img = (ImageFile) imgFile.getBiggest();
 		ImageSize imgSize = img.getSize(); 
 		%>
   "filename" : "<%= imgFile.getName() %>",
--- a/client/digitallibrary/WEB-INF/digilib-config.xml	Tue Mar 01 17:12:25 2011 +0100
+++ b/client/digitallibrary/WEB-INF/digilib-config.xml	Tue Mar 01 22:00:50 2011 +0100
@@ -23,7 +23,7 @@
   <parameter name="subsample-minimum" value="2"/>
   
   <!-- default interpolation quality (0=worst) -->
-  <parameter name="default-quality" value="1"/>
+  <parameter name="default-quality" value="2"/>
   
   <!-- is sending whole image files with mo=file allowed? -->
   <parameter name="sendfile-allowed" value="true" />
@@ -68,5 +68,7 @@
   <!-- logo for PDFs -->
   <parameter name="pdf-logo" value="http://www.mpiwg-berlin.mpg.de/de/images/logo.png" />
 
+  <!-- is the image toolkit allowed to use a disk cache -->
+  <parameter name="img-diskcache-allowed" value="false"/>
   
 </digilib-config>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/digitallibrary/WEB-INF/web-2.3.xml	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
+ 
+<web-app>
+  <!-- General description of your web application -->
+  <display-name>
+        digilib
+  </display-name>
+  <description>
+        This is the web frontend of the Digital Document Library.
+  </description>
+  <!-- The Intialisation Listener -->
+  <listener>
+        <listener-class>
+            digilib.servlet.Initialiser
+        </listener-class>
+  </listener>
+  <!-- The Scaler servlet -->
+  <servlet>
+        <servlet-name>
+            Scaler
+        </servlet-name>
+        <servlet-class>
+            digilib.servlet.ScalerNoAsync
+        </servlet-class>
+        <!-- Load this servlet at server startup time -->
+        <load-on-startup>
+            5
+        </load-on-startup>
+  </servlet>
+  <!-- The Texter servlet -->
+  <servlet>
+        <servlet-name>
+            Texter
+        </servlet-name>
+        <servlet-class>
+            digilib.servlet.Texter
+        </servlet-class>
+  </servlet>
+  <!-- The PDFCache servlet -->
+  <servlet>
+        <servlet-name>
+            PDFCache
+        </servlet-name>
+        <servlet-class>
+            digilib.servlet.PDFCache
+        </servlet-class>
+  </servlet>
+  <!-- The mapping for the Scaler servlet -->
+  <servlet-mapping>
+        <servlet-name>
+            Scaler
+        </servlet-name>
+        <url-pattern>
+            /servlet/Scaler/*
+        </url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+        <servlet-name>
+            Scaler
+        </servlet-name>
+        <url-pattern>
+            /Scaler
+        </url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+        <servlet-name>
+            Scaler
+        </servlet-name>
+        <url-pattern>
+            /authenticated/servlet/Scaler/*
+        </url-pattern>
+  </servlet-mapping>
+  <!-- The mapping for the Texter servlet -->
+  <servlet-mapping>
+        <servlet-name>
+            Texter
+        </servlet-name>
+        <url-pattern>
+            /servlet/Texter/*
+        </url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+        <servlet-name>
+            Texter
+        </servlet-name>
+        <url-pattern>
+            /authenticated/servlet/Texter/*
+        </url-pattern>
+  </servlet-mapping>
+  <!-- The mapping for the Texter servlet -->
+  <servlet-mapping>
+        <servlet-name>
+            PDFCache
+        </servlet-name>
+        <url-pattern>
+            /servlet/PDFCache/*
+        </url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+        <servlet-name>
+            PDFCache
+        </servlet-name>
+        <url-pattern>
+            /authenticated/servlet/PDFCache/*
+        </url-pattern>
+  </servlet-mapping>
+  <!-- region for authenticated access -->
+  <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>
+                Authenticated Digilib
+            </web-resource-name>
+            <url-pattern>
+                /authenticated/*
+            </url-pattern>
+        </web-resource-collection>
+        <!-- we need a default user -->
+<!--         <auth-constraint>
+            <role-name>
+                user
+            </role-name>
+        </auth-constraint>  -->
+  </security-constraint>
+  <login-config>
+  <!--
+        <auth-method>BASIC</auth-method>
+        <realm-name>digilib</realm-name> 
+   -->
+        <auth-method>
+            FORM
+        </auth-method>
+        <form-login-config>
+            <form-login-page>
+                /digilib-login.html
+            </form-login-page>
+            <form-error-page>
+                /digilib-fail.html
+            </form-error-page>
+        </form-login-config>
+  </login-config>
+</web-app>
--- a/client/digitallibrary/WEB-INF/web.xml	Tue Mar 01 17:12:25 2011 +0100
+++ b/client/digitallibrary/WEB-INF/web.xml	Tue Mar 01 22:00:50 2011 +0100
@@ -1,8 +1,10 @@
-<!DOCTYPE web-app 
-    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" 
-    "http://java.sun.com/dtd/web-app_2_3.dtd">
-
-<web-app>
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app
+        version="3.0"
+        xmlns="http://java.sun.com/xml/ns/javaee"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
+        
   <!-- General description of your web application -->
   <display-name>
         digilib
@@ -10,30 +12,17 @@
   <description>
         This is the web frontend of the Digital Document Library.
   </description>
-  <!-- The Initaliser servlet -->
-  <servlet>
-        <servlet-name>
-            Initialiser
-        </servlet-name>
-        <description>
-            The inialisation servlet (must run first).
-        </description>
-        <servlet-class>
+  <!-- The Intialisation Listener (also configured by annotation) -->
+  <listener>
+        <listener-class>
             digilib.servlet.Initialiser
-        </servlet-class>
-        <!-- Load this servlet at server startup time -->
-        <load-on-startup>
-            1
-        </load-on-startup>
-  </servlet>
-  <!-- The Scaler servlet -->
+        </listener-class>
+  </listener>
+  <!-- The Scaler servlet (also configured by annotation) -->
   <servlet>
         <servlet-name>
             Scaler
         </servlet-name>
-        <description>
-            The servlet to scale the digilib images.
-        </description>
         <servlet-class>
             digilib.servlet.Scaler
         </servlet-class>
@@ -47,9 +36,6 @@
         <servlet-name>
             Texter
         </servlet-name>
-        <description>
-            The servlet for text.
-        </description>
         <servlet-class>
             digilib.servlet.Texter
         </servlet-class>
@@ -59,9 +45,6 @@
         <servlet-name>
             PDFCache
         </servlet-name>
-        <description>
-            The servlet for PDF.
-        </description>
         <servlet-class>
             digilib.servlet.PDFCache
         </servlet-class>
@@ -80,6 +63,14 @@
             Scaler
         </servlet-name>
         <url-pattern>
+            /Scaler
+        </url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+        <servlet-name>
+            Scaler
+        </servlet-name>
+        <url-pattern>
             /authenticated/servlet/Scaler/*
         </url-pattern>
   </servlet-mapping>
--- a/client/digitallibrary/digimage_tbl_inc.jsp	Tue Mar 01 17:12:25 2011 +0100
+++ b/client/digitallibrary/digimage_tbl_inc.jsp	Tue Mar 01 22:00:50 2011 +0100
@@ -1,6 +1,6 @@
 <%@ page language="java" %><%
 // retrieve objects from context
-digilib.servlet.DocumentBean docBean = (digilib.servlet.DocumentBean) pageContext.getAttribute("docBean", pageContext.REQUEST_SCOPE);
+digilib.servlet.DocumentBean docBean = (digilib.servlet.DocumentBean) pageContext.getAttribute("docBean", PageContext.REQUEST_SCOPE);
 digilib.servlet.DigilibRequest dlRequest = docBean.getRequest();
 String ua = request.getHeader("User-Agent");
 boolean isN4 = ((ua.indexOf("Mozilla/4.") > -1)&&(ua.indexOf("MSIE") == -1));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/digitallibrary/jquery/digilib.html	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,42 @@
+<?xml version="1.0" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <title>Digilib jQuery: fullscreen</title>
+
+        <style type="text/css">
+            body {
+                 background: silver;
+            }
+        </style>
+
+        <script type="text/javascript" src="jquery-1.4.4.js"></script>
+        <script type="text/javascript" src="jquery.cookie.js"></script>
+        <script type="text/javascript" src="jquery.digilib.js"></script>
+        <script type="text/javascript" src="jquery.digilib.geometry.js"></script>
+        <script type="text/javascript" src="jquery.digilib.birdseye.js"></script>
+        <script type="text/javascript" src="jquery.digilib.regions.js"></script>
+<!--        <script type="text/javascript" src="jquery.digilib.pluginstub.js"></script> -->
+        <link rel="stylesheet" type="text/css" href="jquery.digilib.css" />
+
+
+        <script type="text/javascript">
+            $(document).ready(function(){
+                var opts = {
+                    interactionMode : 'fullscreen'
+                    };
+                var $div = $('div.digilib');
+                $div.digilib(opts);
+            });
+
+        </script>
+    </head>
+
+    <body>
+        <div id="digilib" class="digilib">
+            <p>digilib doesn't work! Please switch on Javascript or notify the server administrator!</p>
+            <img src="http://digilib.berlios.de/images/digilib-logo-big.png" />
+        </div>
+    </body>
+</html>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/DigilibServlet.jardesc	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="MacRoman" standalone="no"?>
+<jardesc>
+    <jar path="digilib-servlet/DigilibServlet.jar"/>
+    <options buildIfNeeded="true" compress="true" descriptionLocation="/digilib-servlet/DigilibServlet.jardesc" exportErrors="false" exportWarnings="true" includeDirectoryEntries="false" overwrite="true" saveDescription="true" storeRefactorings="false" useSourceFolders="false"/>
+    <storedRefactorings deprecationInfo="true" structuralOnly="false"/>
+    <selectedProjects/>
+    <manifest generateManifest="true" manifestLocation="" manifestVersion="1.0" reuseManifest="false" saveManifest="false" usesManifest="true">
+        <sealing sealJar="false">
+            <packagesToSeal/>
+            <packagesToUnSeal/>
+        </sealing>
+    </manifest>
+    <selectedElements exportClassFiles="true" exportJavaFiles="false" exportOutputFolder="false">
+        <javaElement handleIdentifier="=digilib-servlet/src&lt;digilib.util"/>
+        <javaElement handleIdentifier="=digilib-servlet/src&lt;digilib.auth"/>
+        <file path="/digilib-servlet/license.txt"/>
+        <javaElement handleIdentifier="=digilib-servlet/src&lt;digilib.meta"/>
+        <javaElement handleIdentifier="=digilib-servlet/src&lt;digilib.pdf"/>
+        <javaElement handleIdentifier="=digilib-servlet/src&lt;digilib.io"/>
+        <file path="/digilib-servlet/DigilibServlet.jardesc"/>
+        <javaElement handleIdentifier="=digilib-servlet/src&lt;digilib.servlet"/>
+        <javaElement handleIdentifier="=digilib-servlet/src&lt;digilib.image"/>
+    </selectedElements>
+</jardesc>
--- a/servlet/src/digilib/auth/HashTree.java	Tue Mar 01 17:12:25 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +0,0 @@
-/*  HashTree -- Tree in a Hashtable
-
- Digital Image Library servlet components
-
- Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
-
- This program is free software; you can redistribute  it and/or modify it
- under  the terms of  the GNU General  Public License as published by the
- Free Software Foundation;  either version 2 of the  License, or (at your
- option) any later version.
- 
- Please read license.txt for the full details. A copy of the GPL
- may be found at http://www.gnu.org/copyleft/lgpl.html
-
- You should have received a copy of the GNU General Public License
- along with this program; if not, write to the Free Software
- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
- */
-
-package digilib.auth;
-
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.StringTokenizer;
-
-/**
- * Tree representation wrapper for a HashMap.
- * 
- * The HashTree is constructed from a HashMap filled with 'branches' with
- * 'leaves'. The branches are stored as String keys in the HashMap. The String
- * values are leaves.
- * 
- * Branches are matched in 'twigs' separated by 'twig separator' Strings. The
- * return values for a match are leaf values separated by 'leaf separator'
- * Strings.
- * 
- * @author casties
- */
-public class HashTree {
-
-    private Map<String, String> table;
-
-    private String twigSep = "/";
-
-    private String leafSep = ",";
-
-    /**
-     * Constructor of a HashTree.
-     * 
-     * Creates a HashTree wrapper around a given HashMap, using the given twig
-     * separator and leaf separator.
-     * 
-     * @param t
-     * @param twig_separator
-     * @param leaf_separator
-     */
-    public HashTree(Map<String, String> t, String twig_separator, String leaf_separator) {
-        table = t;
-        twigSep = twig_separator;
-        leafSep = leaf_separator;
-        optimizeTable();
-    }
-
-    void optimizeTable() {
-    }
-
-    /**
-     * Matches the given branch against the HashTree.
-     * 
-     * Returns a LinkedList of all leaves on all matching branches in the tree.
-     * Branches in the tree match if they are substrings starting at the same
-     * root.
-     * 
-     * @param branch
-     * @return
-     */
-    List<String> match(String branch) {
-        String b = "";
-        String m;
-        LinkedList<String> matches = new LinkedList<String>();
-
-        // split branch
-        StringTokenizer twig = new StringTokenizer(branch, twigSep);
-        // walk branch and check with tree
-        while (twig.hasMoreTokens()) {
-            if (b.length() == 0) {
-                b = twig.nextToken();
-            } else {
-                b += twigSep + twig.nextToken();
-            }
-            m = table.get(b);
-            if (m != null) {
-                if (m.indexOf(leafSep) < 0) {
-                    // single leaf
-                    matches.add(m);
-                } else {
-                    // split leaves
-                    StringTokenizer leaf = new StringTokenizer(m, leafSep);
-                    while (leaf.hasMoreTokens()) {
-                        matches.add(leaf.nextToken());
-                    }
-                }
-            }
-        }
-        if (matches.size() > 0) {
-            return matches;
-        } else {
-            return null;
-        }
-    }
-}
--- a/servlet/src/digilib/auth/XMLAuthOps.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/auth/XMLAuthOps.java	Tue Mar 01 22:00:50 2011 +0100
@@ -26,8 +26,9 @@
 
 import javax.servlet.http.HttpServletRequest;
 
-import digilib.io.XMLListLoader;
 import digilib.servlet.DigilibRequest;
+import digilib.util.HashTree;
+import digilib.util.XMLListLoader;
 
 /** Implementation of AuthOps using XML files.
  *
--- a/servlet/src/digilib/image/DocuImage.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/image/DocuImage.java	Tue Mar 01 22:00:50 2011 +0100
@@ -27,8 +27,9 @@
 
 import javax.servlet.ServletException;
 
-import digilib.io.ImageFile;
 import digilib.io.FileOpException;
+import digilib.io.ImageInput;
+import digilib.util.ImageSize;
 
 /** The basic class for the representation of a digilib image.
  *
@@ -41,12 +42,12 @@
 
 	/** Loads an image file into the Object.
 	 * 
-	 * @param f Image File.
+	 * @param ii Image File.
 	 * @throws FileOpException Exception thrown if any error occurs.
 	 */
-	public void loadImage(ImageFile f) throws FileOpException;
+	public void loadImage(ImageInput ii) throws FileOpException;
 
-	/** This DocuImage support the loadSubImage operation.
+	/** This DocuImage supports the loadSubImage operation.
 	 * 
 	 * @return boolean
 	 */
@@ -54,23 +55,23 @@
 
 	/** Load only a subsampled region of the image file.
 	 * 
-	 * @param f
+	 * @param ii
 	 * @param region
 	 * @param subsample
 	 * @throws FileOpException
 	 */
-	public void loadSubimage(ImageFile f, Rectangle region, int subsample)
+	public void loadSubimage(ImageInput ii, Rectangle region, int subsample)
 		throws FileOpException;
 
-	/** Writes the current image to a ServletResponse.
+	/** Writes the current image to an OutputStream.
 	 *
 	 * The image is encoded to the mime-type <code>mt</code> and sent to the output
-	 * stream of the <code>ServletResponse</code> <code>res</code>.
+	 * stream <code>ostream</code>.
 	 *
 	 * Currently only mime-types "image/jpeg" and "image/png" are supported.
 	 * 
 	 * @param mt mime-type of the image to be sent.
-	 * @param res ServletResponse where the image is sent.
+	 * @param ostream OutputStream where the image is sent.
 	 * @throws ServletException Exception thrown on sending data.
 	 * @throws ImageOpException Exception in other cases.
 	 */
@@ -95,7 +96,7 @@
 	 */
 	public ImageSize getSize();
 
-	/** The mime-type of the current image.
+	/** The mime-type of the image, i.e. the mime-type of the input that was read.
 	 * 
 	 * @return String the mime-type of this image.
 	 */
@@ -201,6 +202,22 @@
 	public void enhanceRGB(float[] rgbm, float[] rgba)
 		throws ImageOpException;
 
+
+	/** Operations for colorOps.
+	 * 
+	 *
+	 */
+	public enum ColorOp {GRAYSCALE, INVERT};
+
+	/** Changes the colors of the current image.
+	 * 
+	 * Changes the colors of the current image. Operations are e.g.
+	 * reduction to grayscale or false-color palettes.
+	 * 
+	 * @throws ImageOpException
+	 */
+	public void colorOp(ColorOp op) throws ImageOpException;
+
 	/**
 	 * Returns the interpolation quality.
 	 * @return int
@@ -221,9 +238,9 @@
 	public void dispose();
 
     /**
-     * Check image size and type and store in ImageFile f
+     * Check image size and type and store in ImageInput ii
      */
-    public ImageFile identify(ImageFile imgf) throws IOException;
+    public ImageInput identify(ImageInput ii) throws IOException;
 
     /**
      * Returns a list of supported image formats
--- a/servlet/src/digilib/image/DocuImageImpl.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/image/DocuImageImpl.java	Tue Mar 01 22:00:50 2011 +0100
@@ -33,7 +33,8 @@
 import org.apache.log4j.Logger;
 
 import digilib.io.FileOpException;
-import digilib.io.ImageFile;
+import digilib.io.ImageInput;
+import digilib.util.ImageSize;
 
 /** Simple abstract implementation of the <code>DocuImage</code> interface.
  *
@@ -53,13 +54,13 @@
 	
 	/** epsilon for float comparisons. */
 	public final double epsilon = 1e-5;
-	
-	/** image mime-type */
-	protected String mimeType = null;
 
 	/** image size */
     protected ImageSize imgSize = null;
 
+    /** ImageInput that was read */
+    protected ImageInput input;
+
 	/**
 	 * Returns the quality.
 	 * @return int
@@ -99,14 +100,20 @@
 		scale(scale, scale);
 	}
 	
+	/* (non-Javadoc)
+	 * @see digilib.image.DocuImage#getMimetype()
+	 */
 	public String getMimetype() {
-		return mimeType;
+	    if (input != null) {
+	        return input.getMimetype();
+	    }
+	    return null;
 	}
 
     /* (non-Javadoc)
      * @see digilib.image.DocuImage#identify(digilib.io.ImageFile)
      */
-    public ImageFile identify(ImageFile imgf) throws IOException {
+    public ImageInput identify(ImageInput ii) throws IOException {
         // just a do-nothing implementation
         return null;
     }
@@ -128,7 +135,7 @@
 		return false;
 	}
 
-	public void loadSubimage(ImageFile f, Rectangle region, int subsample)
+	public void loadSubimage(ImageInput ii, Rectangle region, int subsample)
 		throws FileOpException {
 		// empty implementation
 	}
@@ -138,6 +145,10 @@
 		// emtpy implementation
 	}
 
+	public void colorOp(ColorOp op) throws ImageOpException {
+		// emtpy implementation
+	}
+
 	public void dispose() {
 		// emtpy implementation
 	}
@@ -149,11 +160,11 @@
 
     public void crop(int xoff, int yoff, int width, int height)
             throws ImageOpException {
-        // TODO Auto-generated method stub
+        // emtpy implementation
     }
 
     public Image getAwtImage() {
-        // TODO Auto-generated method stub
+        // emtpy implementation
         return null;
     }
 
@@ -177,19 +188,12 @@
         return imgSize;
     }
 
-    public void loadImage(ImageFile f) throws FileOpException {
-        // TODO Auto-generated method stub
-        
-    }
+    public abstract void loadImage(ImageInput ii) throws FileOpException;
+
+    public abstract void scale(double scaleX, double scaleY) throws ImageOpException;
 
-    public void scale(double scaleX, double scaleY) throws ImageOpException {
-        // TODO Auto-generated method stub
-        
-    }
+    public abstract void writeImage(String mt, OutputStream ostream)
+            throws ServletException, ImageOpException;
 
-    public void writeImage(String mt, OutputStream ostream)
-            throws ServletException, ImageOpException {
-        // TODO Auto-generated method stub
-    }
 	
 }
--- a/servlet/src/digilib/image/ImageInfoDocuImage.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/image/ImageInfoDocuImage.java	Tue Mar 01 22:00:50 2011 +0100
@@ -3,13 +3,13 @@
  */
 package digilib.image;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.RandomAccessFile;
 
 import org.marcoschmidt.image.ImageInfo;
 
-import digilib.io.ImageFile;
+import digilib.io.ImageInput;
+import digilib.util.ImageSize;
 
 /** Simple abstract implementation of the <code>DocuImage</code> interface.
  * Implements only the identify method using the ImageInfo class.
@@ -18,31 +18,38 @@
  */
 public abstract class ImageInfoDocuImage extends DocuImageImpl {
 
-    /** Check image size and type and store in ImageFile f */
-    public ImageFile identify(ImageFile imgf) throws IOException {
-        // fileset to store the information
-        File f = imgf.getFile();
-        if (f == null) {
-            throw new IOException("File not found!");
-        }
-        RandomAccessFile raf = new RandomAccessFile(f, "r");
-        // set up ImageInfo object
-        ImageInfo iif = new ImageInfo();
-        iif.setInput(raf);
-        iif.setCollectComments(false);
-        iif.setDetermineImageNumber(false);
-        logger.debug("identifying (ImageInfo) " + f);
-        // try with ImageInfo first
-        if (iif.check()) {
-            ImageSize d = new ImageSize(iif.getWidth(), iif.getHeight());
-            imgf.setSize(d);
-            imgf.setMimetype(iif.getMimeType());
-            //logger.debug("  format:"+iif.getFormatName());
-            raf.close();
-            logger.debug("image size: " + imgf.getSize());
-            return imgf;
-        } else {
-            raf.close();
+    /* Check image size and type and store in ImageFile f */
+    public ImageInput identify(ImageInput ii) throws IOException {
+        logger.debug("identifying (ImageInfo) " + ii);
+        RandomAccessFile raf = null;
+        try {
+            // set up ImageInfo object
+            ImageInfo iif = new ImageInfo();
+            if (ii.hasImageInputStream()) {
+                iif.setInput(ii.getImageInputStream());
+            } else if (ii.hasFile()) {
+                raf = new RandomAccessFile(ii.getFile(), "r");
+                iif.setInput(raf);
+            } else {
+                return null;
+            }
+            iif.setCollectComments(false);
+            iif.setDetermineImageNumber(false);
+            // try with ImageInfo first
+            if (iif.check()) {
+                ImageSize d = new ImageSize(iif.getWidth(), iif.getHeight());
+                ii.setSize(d);
+                ii.setMimetype(iif.getMimeType());
+                logger.debug("image size: " + ii.getSize());
+                return ii;
+            }
+        } catch (Exception e) {
+            logger.debug("ImageInfoDocuimage unable to identify.", e);
+        } finally {
+            // close file, don't close stream(?)
+            if (raf != null) {
+                raf.close();
+            }
         }
         return null;
     }
--- a/servlet/src/digilib/image/ImageJobDescription.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/image/ImageJobDescription.java	Tue Mar 01 22:00:50 2011 +0100
@@ -6,29 +6,29 @@
 
 import org.apache.log4j.Logger;
 
+import digilib.image.DocuImage.ColorOp;
 import digilib.io.DocuDirCache;
 import digilib.io.DocuDirectory;
 import digilib.io.FileOpException;
 import digilib.io.FileOps;
 import digilib.io.FileOps.FileClass;
-import digilib.io.ImageFile;
-import digilib.io.ImageFileset;
+import digilib.io.ImageInput;
+import digilib.io.ImageSet;
 import digilib.servlet.DigilibConfiguration;
+import digilib.util.ImageSize;
 import digilib.util.OptionsSet;
 import digilib.util.Parameter;
 import digilib.util.ParameterMap;
 
-
-/** 
- * A container class for storing a set of instructional parameters 
- * used for content generating classes like MakePDF.  
+/**
+ * A class for storing the set of parameters necessary for scaling images 
+ * with an ImageWorker.
  * 
- * This contains the functionality formerly found in Scaler, processRequest, only factorized.
- * 
- * TODO clean up...
+ * This contains the functionality formerly found in Scaler.processRequest(),
+ * only factorized.
  * 
  * @author cmielack, casties
- *
+ * 
  */
 
 public class ImageJobDescription extends ParameterMap {
@@ -36,18 +36,18 @@
 	DigilibConfiguration dlConfig = null;
 	protected static Logger logger = Logger.getLogger("digilib.servlet");
 
-	ImageFile fileToLoad = null;
-	ImageFileset fileset = null;
+	ImageInput input = null;
+	ImageSet imageSet = null;
 	DocuDirectory fileDir = null;
 	String filePath = null;
 	ImageSize expectedSourceSize = null;
 	Float scaleXY = null;
 	Rectangle2D userImgArea = null;
-	Rectangle2D outerUserImgArea= null;
+	Rectangle2D outerUserImgArea = null;
 	Boolean imageSendable = null;
-	String mimeType;
-	Integer paramDW;
-	Integer paramDH;
+	String mimeType = null;
+	Integer paramDW = null;
+	Integer paramDH = null;
 
 	/** create empty ImageJobDescription.
 	 * @param dlcfg
@@ -101,6 +101,8 @@
 		newParameter("ddpiy", new Float(0), null, 's');
 		// scale factor for mo=ascale
 		newParameter("scale", new Float(1), null, 's');
+		// color conversion operation
+		newParameter("colop", "", null, 's');
 	}
 
 
@@ -128,48 +130,57 @@
 	}
 
 	
+	/** Returns the mime-type (of the input). 
+	 * @return
+	 * @throws IOException
+	 */
 	public String getMimeType() throws IOException {
 		if (mimeType == null) {
-			fileToLoad = getFileToLoad();
-			if(! fileToLoad.isChecked()){
-				DigilibConfiguration.docuImageIdentify(fileToLoad);
-			}
-			mimeType = fileToLoad.getMimetype();
+			input = getInput();
+			mimeType = input.getMimetype();
 		}
 		return mimeType;
 	}
 	
-	public ImageFile getFileToLoad() throws IOException {
-		
-		if(fileToLoad == null){
-			fileset = getFileset();
+	/** Returns the ImageInput to use.
+	 * @return
+	 * @throws IOException
+	 */
+	public ImageInput getInput() throws IOException {
+		if(input == null){
+			imageSet = getImageSet();
 			
 			/* select a resolution */
-			if (getHiresOnly()) {
+			if (isHiresOnly()) {
 				// get first element (= highest resolution)
-				fileToLoad = fileset.getBiggest();
-			} else if (getLoresOnly()) {
+				input = imageSet.getBiggest();
+			} else if (isLoresOnly()) {
 				// enforced lores uses next smaller resolution
-				fileToLoad = fileset.getNextSmaller(getExpectedSourceSize());
-				if (fileToLoad == null) {
+				input = imageSet.getNextSmaller(getExpectedSourceSize());
+				if (input == null) {
 					// this is the smallest we have
-					fileToLoad = fileset.getSmallest();
+					input = imageSet.getSmallest();
 				}
 			} else {
 				// autores: use next higher resolution
-				fileToLoad = fileset.getNextBigger(getExpectedSourceSize());
-				if (fileToLoad == null) {
+				input = imageSet.getNextBigger(getExpectedSourceSize());
+				if (input == null) {
 					// this is the highest we have
-					fileToLoad = fileset.getBiggest();
+					input = imageSet.getBiggest();
 				}
 			}
-			logger.info("Planning to load: " + fileToLoad.getFile());
+			if (input == null || input.getMimetype() == null) {
+			    throw new FileOpException("Unable to load "+input);
+			}
+            logger.info("Planning to load: " + input);
 		}
-		
-		return fileToLoad;
-
+		return input;
 	}
 	
+	/** Returns the DocuDirectory for the input (file). 
+	 * @return
+	 * @throws FileOpException
+	 */
 	public DocuDirectory getFileDirectory() throws FileOpException {
 		if(fileDir == null){
 			DocuDirCache dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
@@ -182,19 +193,26 @@
 		return fileDir;
 	}
 	
-    public ImageFileset getFileset() throws FileOpException {
-        if(fileset==null){
+    /** Returns the ImageSet to load.
+     * @return
+     * @throws FileOpException
+     */
+    public ImageSet getImageSet() throws FileOpException {
+        if(imageSet==null){
             DocuDirCache dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
     
-            fileset = (ImageFileset) dirCache.getFile(getFilePath(), getAsInt("pn"), FileClass.IMAGE);
-            if (fileset == null) {
+            imageSet = (ImageSet) dirCache.getFile(getFilePath(), getAsInt("pn"), FileClass.IMAGE);
+            if (imageSet == null) {
                 throw new FileOpException("File " + getFilePath() + "("
                         + getAsInt("pn") + ") not found.");
             }
         }
-        return fileset;
+        return imageSet;
     }
     
+	/** Returns the file path name from the request.
+	 * @return
+	 */
 	public String getFilePath() {
 		if(filePath == null){
 			String s = this.getAsString("request.path");
@@ -204,32 +222,36 @@
 		return filePath;
 	}
 
-	public boolean getHiresOnly(){
+	public boolean isHiresOnly(){
 		return hasOption("clip") || hasOption("hires");
 	}
 	
-	public boolean getLoresOnly(){
+	public boolean isLoresOnly(){
 		return hasOption("lores");
 	}
 
-	public boolean getScaleToFit() {
+	public boolean isScaleToFit() {
 		return !(hasOption("clip") || hasOption("osize") || hasOption("ascale"));
 	}
 
-	public boolean getAbsoluteScale(){
+	public boolean isAbsoluteScale(){
 		return hasOption("osize") || hasOption("ascale");
 	}
 	
 	
+	/** Returns the minimum size the source image should have for scaling.
+	 * @return
+	 * @throws IOException
+	 */
 	public ImageSize getExpectedSourceSize() throws IOException {
 		if (expectedSourceSize == null){
 			expectedSourceSize = new ImageSize();
-			if (getScaleToFit()) {
+			if (isScaleToFit()) {
 				// scale to fit -- calculate minimum source size
 				float scale = (1 / Math.min(getAsFloat("ww"), getAsFloat("wh"))) * getAsFloat("ws");
 				expectedSourceSize.setSize((int) (getDw() * scale),
 						(int) (getDh() * scale));
-			} else if (getAbsoluteScale() && hasOption("ascale")) {
+			} else if (isAbsoluteScale() && hasOption("ascale")) {
 				// absolute scale -- apply scale to hires size
 				expectedSourceSize = getHiresSize().getScaled(getAsFloat("scale"));
 			} else {
@@ -241,16 +263,17 @@
 		return expectedSourceSize;
 	}
 	
+	/** Returns the size of the highest resolution image.
+	 * @return
+	 * @throws IOException
+	 */
 	public ImageSize getHiresSize() throws IOException {
 		logger.debug("get_hiresSize()");
 
 		ImageSize hiresSize = null;
-		ImageFileset fileset = getFileset();
-		if (getAbsoluteScale()) {
-			ImageFile hiresFile = fileset.getBiggest();
-			if (!hiresFile.isChecked()) {
-				DigilibConfiguration.docuImageIdentify(hiresFile);
-			}
+		ImageSet fileset = getImageSet();
+		if (isAbsoluteScale()) {
+			ImageInput hiresFile = fileset.getBiggest();
 			hiresSize = hiresFile.getSize();
 		}
 		return hiresSize;
@@ -270,7 +293,7 @@
 			float areaWidth;
 			float areaHeight;
 			float ws = getAsFloat("ws");
-			ImageSize imgSize = getFileToLoad().getSize();
+			ImageSize imgSize = getInput().getSize();
 			// user window area in [0,1] coordinates
 			Rectangle2D relUserArea = new Rectangle2D.Float(getAsFloat("wx"), getAsFloat("wy"),
 					getAsFloat("ww"), getAsFloat("wh"));
@@ -281,20 +304,20 @@
 			userImgArea = imgTrafo.createTransformedShape(
 					relUserArea).getBounds2D();
 	
-			if (getScaleToFit()) {
+			if (isScaleToFit()) {
 				// calculate scaling factors based on inner user area
 				areaWidth = (float) userImgArea.getWidth();
 				areaHeight = (float) userImgArea.getHeight();
 				float scaleX = getDw() / areaWidth * ws;
 				float scaleY = getDh() / areaHeight * ws;
 				scaleXY = (scaleX > scaleY) ? scaleY : scaleX;
-			} else if (getAbsoluteScale()) {
+			} else if (isAbsoluteScale()) {
 				// absolute scaling factor
 				if (hasOption("osize")) {
 					// get original resolution from metadata
-					fileset.checkMeta();
-					float origResX = fileset.getResX();
-					float origResY = fileset.getResY();
+					imageSet.checkMeta();
+					float origResX = imageSet.getResX();
+					float origResY = imageSet.getResY();
 					if ((origResX == 0) || (origResY == 0)) {
 						throw new ImageOpException("Missing image DPI information!");
 					}
@@ -340,6 +363,11 @@
 		return (float) scaleXY;
 	}
 	
+	/** Returns the width of the destination image.
+	 * Uses dh parameter and aspect ratio if dw parameter is empty. 
+	 * @return
+	 * @throws IOException
+	 */
 	public int getDw() throws IOException {
 		logger.debug("get_paramDW()");
 		if (paramDW == null) {
@@ -347,7 +375,7 @@
 			paramDW = getAsInt("dw");
 			paramDH = getAsInt("dh");
 
-			float imgAspect = getFileToLoad().getAspect();
+			float imgAspect = getInput().getAspect();
 			if (paramDW == 0) {
 				// calculate dw
 				paramDW = Math.round(paramDH * imgAspect);
@@ -361,6 +389,11 @@
 		return paramDW;
 	}
 	
+	/** Returns the height of the destination image.
+	 * Uses dw parameter and aspect ratio if dh parameter is empty. 
+	 * @return
+	 * @throws IOException
+	 */
 	public int getDh() throws IOException {
 		logger.debug("get_paramDH()");
 		if (paramDH == null) {
@@ -368,7 +401,7 @@
 			paramDW = getAsInt("dw");
 			paramDH = getAsInt("dh");
 
-			float imgAspect = getFileToLoad().getAspect();
+			float imgAspect = getInput().getAspect();
 			if (paramDW == 0) {
 				// calculate dw
 				paramDW = Math.round(paramDH * imgAspect);
@@ -382,9 +415,12 @@
 		return paramDH;
 	}
 	
-	public Integer getScaleQual(){
+	/** Returns image quality as an integer.
+	 * @return
+	 */
+	public int getScaleQual(){
 		logger.debug("get_scaleQual()");
-		Integer qual = dlConfig.getAsInt("default-quality");
+		int qual = dlConfig.getAsInt("default-quality");
 		if(hasOption("q0"))
 			qual = 0;
 		else if(hasOption("q1"))
@@ -394,22 +430,43 @@
 		return qual;
 	}
 
+	public ColorOp getColOp() {
+		String op = getAsString("colop");
+		try {
+			return ColorOp.valueOf(op.toUpperCase());
+		} catch (Exception e) {
+			logger.error("Invalid color op: " + op);
+		}
+		return null;
+	}
 	
+	/**
+	 * Returns the area of the source image that will be transformed into the
+	 * destination image.
+	 * 
+	 * @return
+	 * @throws IOException
+	 * @throws ImageOpException
+	 */
 	public Rectangle2D getUserImgArea() throws IOException, ImageOpException{
 		if(userImgArea == null) {
 			// getScaleXY sets userImgArea
 			getScaleXY();
 		}
 		return userImgArea;		
-		
 	}
 	
+	/** Returns the maximal area of the source image that will be used.
+	 * @return
+	 * @throws IOException
+	 * @throws ImageOpException
+	 */
 	public Rectangle2D getOuterUserImgArea() throws IOException, ImageOpException {
 		if(outerUserImgArea == null){
 			outerUserImgArea = getUserImgArea();
 			
 			// image size in pixels
-			ImageSize imgSize = getFileToLoad().getSize();
+			ImageSize imgSize = getInput().getSize();
 			Rectangle2D imgBounds = new Rectangle2D.Float(0, 0, imgSize.getWidth(), 
 					imgSize.getHeight());
 			
@@ -437,10 +494,7 @@
 		float[] paramRGBM = null;//{0f,0f,0f};
 		Parameter p = params.get("rgbm");
 		if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
-			paramRGBM = p.parseAsFloatArray("/");
-			if ((paramRGBM == null) || (paramRGBM.length != 3)) {
-			    return null;
-			}
+			return p.parseAsFloatArray("/");
 		}	
 		return paramRGBM;
 	}
@@ -450,9 +504,6 @@
 		Parameter p = params.get("rgba");
 		if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
 			paramRGBA = p.parseAsFloatArray("/");
-            if ((paramRGBA == null) || (paramRGBA.length != 3)) {
-                return null;
-            }
 		}
 		return paramRGBA;
 	}
@@ -465,42 +516,45 @@
 		|| hasOption("rawfile");
 	}
 	
-	/** Could the image be sent without processing?
-	 * Takes image type and additional image operations into account. 
-	 * Does not check requested size transformation.
-	 * @return
-	 * @throws IOException 
-	 */
-	public boolean isImageSendable() throws IOException {
-		// cached result?
-		if (imageSendable == null) {
-			String mimeType = getMimeType();
-			imageSendable = ( (mimeType.equals("image/jpeg")
-				        	|| mimeType.equals("image/png")
-				        	|| mimeType.equals("image/gif") )
-				        	&& 
-				        	!(hasOption("hmir")
-							|| hasOption("vmir") 
-							|| (getAsFloat("rot") != 0.0)
-							|| (getRGBM() != null) 
-							|| (getRGBA() != null)
-							|| (getAsFloat("cont") != 0.0) 
-							|| (getAsFloat("brgt") != 0.0)));
-		}
-		
-		return imageSendable;
-	}
+    /**
+     * Returns if the image can be sent without processing. Takes image type and
+     * additional image operations into account. Does not check requested size
+     * transformation.
+     * 
+     * @return
+     * @throws IOException
+     */
+    public boolean isImageSendable() throws IOException {
+        if (imageSendable == null) {
+            String mimeType = getMimeType();
+            imageSendable = (mimeType != null
+                    && (mimeType.equals("image/jpeg") || mimeType.equals("image/png") 
+                            || mimeType.equals("image/gif"))
+                    && !(hasOption("hmir")
+                    || hasOption("vmir") || (getAsFloat("rot") != 0.0)
+                    || (getRGBM() != null) || (getRGBA() != null)
+                    || (getAsFloat("cont") != 0.0) || (getAsFloat("brgt") != 0.0)));
+        }
+        return imageSendable;
+    }
 	
 	
+	/**
+	 * Returns if any transformation of the source image (image manipulation or
+	 * format conversion) is required.
+	 * 
+	 * @return
+	 * @throws IOException
+	 */
 	public boolean isTransformRequired() throws IOException {
-		ImageSize is = getFileToLoad().getSize();
+		ImageSize is = getInput().getSize();
 		ImageSize ess = getExpectedSourceSize();
 		// nt = no transform required
 		boolean nt = isImageSendable() && (
 			// lores: send if smaller
-			(getLoresOnly() && is.isSmallerThan(ess))
+			(isLoresOnly() && is.isSmallerThan(ess))
 			// else send if it fits
-			|| (!(getLoresOnly() || getHiresOnly()) && is.fitsIn(ess)));
+			|| (!(isLoresOnly() || isHiresOnly()) && is.fitsIn(ess)));
 		return ! nt;
 	}
 }
\ No newline at end of file
--- a/servlet/src/digilib/image/ImageLoaderDocuImage.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/image/ImageLoaderDocuImage.java	Tue Mar 01 22:00:50 2011 +0100
@@ -2,7 +2,7 @@
 
  Digital Image Library servlet components
 
- Copyright (C) 2002, 2003 Robert Casties (robcast@mail.berlios.de)
+ Copyright (C) 2002 - 2011 Robert Casties (robcast@mail.berlios.de)
 
  This program is free software; you can redistribute  it and/or modify it
  under  the terms of  the GNU General  Public License as published by the
@@ -22,14 +22,19 @@
 import java.awt.Image;
 import java.awt.Rectangle;
 import java.awt.RenderingHints;
+import java.awt.color.ColorSpace;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.Rectangle2D;
 import java.awt.image.AffineTransformOp;
 import java.awt.image.BufferedImage;
+import java.awt.image.ByteLookupTable;
+import java.awt.image.ColorConvertOp;
+import java.awt.image.ColorModel;
 import java.awt.image.ConvolveOp;
 import java.awt.image.Kernel;
+import java.awt.image.LookupOp;
+import java.awt.image.LookupTable;
 import java.awt.image.RescaleOp;
-import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.RandomAccessFile;
@@ -49,24 +54,48 @@
 
 import digilib.io.FileOpException;
 import digilib.io.FileOps;
-import digilib.io.ImageFile;
-import digilib.io.ImageFileset;
+import digilib.io.ImageInput;
+import digilib.util.ImageSize;
 
 /** Implementation of DocuImage using the ImageLoader API of Java 1.4 and Java2D. */
 public class ImageLoaderDocuImage extends ImageInfoDocuImage {
-
+    
 	/** image object */
 	protected BufferedImage img;
 	
 	/** interpolation type */
-	protected RenderingHints renderHint;
+	protected RenderingHints renderHint = null;
+
+	/** convolution kernels for blur() */
+	protected static Kernel[] convolutionKernels = {
+	        null,
+	        new Kernel(1, 1, new float[] {1f}),
+            new Kernel(2, 2, new float[] {0.25f, 0.25f, 0.25f, 0.25f}),
+            new Kernel(3, 3, new float[] {1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f})
+	};
 
-	/** ImageIO image reader */
-	protected ImageReader reader;
-
-	/** File that was read */
-	protected File imgFile;
-
+	/** lookup table for inverting images (byte) */
+	protected static LookupTable invertByteTable;
+    protected static LookupTable invertRGBByteTable;
+	
+	static {
+		byte[] invertByte = new byte[256];
+		byte[] orderedByte = new byte[256];
+		for (int i = 0; i < 256; ++i) {
+			invertByte[i] = (byte) (256 - i);
+			orderedByte[i] = (byte) i;
+		}
+		// works for JPEG in q2
+		invertRGBByteTable = new ByteLookupTable(0, new byte[][] {
+				orderedByte, invertByte, invertByte});
+		// should work for all color models
+		invertByteTable = new ByteLookupTable(0, invertByte);
+	}
+	
+	/** the size of the current image */
+    protected ImageSize imageSize;
+	
+	
 	/* loadSubimage is supported. */
 	public boolean isSubimageSupported() {
 		return true;
@@ -91,25 +120,26 @@
 
     /* returns the size of the current image */
     public ImageSize getSize() {
-        ImageSize is = null;
-        // TODO: do we want to cache imageSize?
-        int h = 0;
-        int w = 0;
-        try {
-            if (img == null) {
-                // get size from ImageReader
-                h = reader.getHeight(0);
-                w = reader.getWidth(0);
-            } else {
-                // get size from image
-                h = img.getHeight();
-                w = img.getWidth();
+        if (imageSize == null) {
+            int h = 0;
+            int w = 0;
+            try {
+                if (img == null) {
+                    ImageReader reader = getReader(input);
+                    // get size from ImageReader
+                    h = reader.getHeight(0);
+                    w = reader.getWidth(0);
+                } else {
+                    // get size from image
+                    h = img.getHeight();
+                    w = img.getWidth();
+                }
+                imageSize = new ImageSize(w, h);
+            } catch (IOException e) {
+                logger.debug("error in getSize:", e);
             }
-            is = new ImageSize(w, h);
-        } catch (IOException e) {
-            logger.debug("error in getSize:", e);
         }
-        return is;
+        return imageSize;
     }
 
 	/* returns a list of supported image formats */
@@ -118,44 +148,56 @@
 		return Arrays.asList(formats).iterator();
 	}
 
-    /** Check image size and type and store in ImageFile f */
-    public ImageFile identify(ImageFile imageFile) throws IOException {
+    /* Check image size and type and store in ImageInput */
+    public ImageInput identify(ImageInput input) throws IOException {
         // try parent method first
-        ImageFile imf = super.identify(imageFile);
-        if (imf != null) {
-            return imf;
-        }
-        // fileset to store the information
-        ImageFileset imgfs = imageFile.getParent();
-        File f = imageFile.getFile();
-        if (f == null) {
-            throw new IOException("File not found!");
+        ImageInput ii = super.identify(input);
+        if (ii != null) {
+            return ii;
         }
-        logger.debug("identifying (ImageIO) " + f);
-        /*
-         * try ImageReader
-         */
-        if ((reader == null) || (imgFile != imageFile.getFile())) {
-            getReader(imageFile);
+        logger.debug("identifying (ImageIO) " + input);
+        ImageReader reader = null;
+        try {
+            /*
+             * try ImageReader
+             */
+            reader = getReader(input);
+            // set size
+            ImageSize d = new ImageSize(reader.getWidth(0), reader.getHeight(0));
+            input.setSize(d);
+            // set mime type
+            if (input.getMimetype() == null) {
+                if (input.hasFile()) {
+                    String t = FileOps.mimeForFile(input.getFile());
+                    input.setMimetype(t);
+                } else {
+                    // FIXME: is format name a mime type???
+                    String t = reader.getFormatName();
+                    input.setMimetype(t);
+                }
+            }
+            return input;
+        } catch (FileOpException e) {
+            // maybe just our class doesn't know what to do
+            logger.error("ImageLoaderDocuimage unable to identify:", e);
+            return null;
+        } finally {
+            if (reader != null) {
+                reader.dispose();
+            }
         }
-        ImageSize d = new ImageSize(reader.getWidth(0), reader.getHeight(0));
-        imageFile.setSize(d);
-        // String t = reader.getFormatName();
-        String t = FileOps.mimeForFile(f);
-        imageFile.setMimetype(t);
-        // logger.debug("  format:"+t);
-        if (imgfs != null) {
-            imgfs.setAspect(d);
-        }
-        return imageFile;
     }
     
     /* load image file */
-	public void loadImage(ImageFile f) throws FileOpException {
-		logger.debug("loadImage " + f.getFile());
+	public void loadImage(ImageInput ii) throws FileOpException {
+		logger.debug("loadImage: " + ii);
+		this.input = ii;
 		try {
-			img = ImageIO.read(f.getFile());
-            mimeType = f.getMimetype();
+		    if (ii.hasImageInputStream()) {
+                img = ImageIO.read(ii.getImageInputStream());
+		    } else if (ii.hasFile()) {
+		        img = ImageIO.read(ii.getFile());
+		    }
 		} catch (IOException e) {
 			throw new FileOpException("Error reading image.");
 		}
@@ -166,17 +208,28 @@
 	 * 
 	 * @return
 	 */
-	public ImageReader getReader(ImageFile f) throws IOException {
-		logger.debug("preloadImage " + f.getFile());
-		if (reader != null) {
-			logger.debug("Reader was not null!");
-			// clean up old reader
-			dispose();
+	public ImageReader getReader(ImageInput input) throws IOException {
+		logger.debug("get ImageReader for " + input);
+		ImageInputStream istream = null;
+		if (input.hasImageInputStream()) {
+			// stream input
+			istream = input.getImageInputStream();
+		} else if (input.hasFile()) {
+			// file only input
+			RandomAccessFile rf = new RandomAccessFile(input.getFile(), "r");
+			istream = new FileImageInputStream(rf);
+		} else {
+			throw new FileOpException("Unable to get data from ImageInput");
 		}
-		RandomAccessFile rf = new RandomAccessFile(f.getFile(), "r");
-		ImageInputStream istream = new FileImageInputStream(rf);
 		Iterator<ImageReader> readers;
-		String mt = f.getMimetype();
+		String mt = null;
+		if (input.hasMimetype()) {
+	        // check hasMimetype first or we might get into a loop
+		    mt = input.getMimetype();
+		} else {
+		    // try file extension
+            mt = FileOps.mimeForFile(input.getFile());
+		}
 		if (mt == null) {
 			logger.debug("No mime-type. Trying automagic.");
 			readers = ImageIO.getImageReaders(istream);
@@ -185,28 +238,26 @@
 			readers = ImageIO.getImageReadersByMIMEType(mt);
 		}
 		if (!readers.hasNext()) {
-		    rf.close();
 			throw new FileOpException("Can't find Reader to load File!");
 		}
-		reader = readers.next();
+		ImageReader reader = readers.next();
 		/* are there more readers? */
 		logger.debug("ImageIO: this reader: " + reader.getClass());
 		/* while (readers.hasNext()) {
 			logger.debug("ImageIO: next reader: " + readers.next().getClass());
 		} */
 		reader.setInput(istream);
-		imgFile = f.getFile();
 		return reader;
 	}
 
 	/* Load an image file into the Object. */
-	public void loadSubimage(ImageFile f, Rectangle region, int prescale)
+	public void loadSubimage(ImageInput ii, Rectangle region, int prescale)
 			throws FileOpException {
 		logger.debug("loadSubimage");
+        this.input = ii;
+        ImageReader reader = null;
 		try {
-			if ((reader == null) || (imgFile != f.getFile())) {
-				getReader(f);
-			}
+			reader = getReader(ii);
 			// set up reader parameters
 			ImageReadParam readParam = reader.getDefaultReadParam();
 			readParam.setSourceRegion(region);
@@ -216,10 +267,13 @@
 			// read image
 			logger.debug("loading..");
 			img = reader.read(0, readParam);
-			mimeType = f.getMimetype();
 			logger.debug("loaded");
 		} catch (IOException e) {
 			throw new FileOpException("Unable to load File!");
+		} finally {
+		    if (reader != null) {
+		        reader.dispose();
+		    }
 		}
 	}
 
@@ -285,16 +339,12 @@
 		} catch (IOException e) {
 		    logger.error("Error writing image:", e);
 			throw new ServletException("Error writing image:", e);
-		} finally {
-			// clean up
-			if (writer != null) {
-				writer.dispose();
-			}
 		}
+		// TODO: should we: finally { writer.dispose(); }
 	}
 
 	public void scale(double scale, double scaleY) throws ImageOpException {
-		logger.debug("scale");
+		logger.debug("scale: " + scale);
 		/* for downscaling in high quality the image is blurred first */
 		if ((scale <= 0.5) && (quality > 1)) {
 			int bl = (int) Math.floor(1 / scale);
@@ -325,19 +375,23 @@
 	}
 
 	public void blur(int radius) throws ImageOpException {
-		// DEBUG
 		logger.debug("blur: " + radius);
 		// minimum radius is 2
 		int klen = Math.max(radius, 2);
-		// FIXME: use constant kernels for most common sizes
-		int ksize = klen * klen;
-		// kernel is constant 1/k
-		float f = 1f / ksize;
-		float[] kern = new float[ksize];
-		for (int i = 0; i < ksize; i++) {
-			kern[i] = f;
+		Kernel blur = null;
+		if (klen < convolutionKernels.length) {
+            blur = convolutionKernels[klen];
+		} else {
+            // calculate our own kernel
+            int ksize = klen * klen;
+            // kernel is constant 1/k
+            float f = 1f / ksize;
+            float[] kern = new float[ksize];
+            for (int i = 0; i < ksize; ++i) {
+                kern[i] = f;
+            }
+            blur = new Kernel(klen, klen, kern);
 		}
-		Kernel blur = new Kernel(klen, klen, kern);
 		// blur with convolve operation
 		ConvolveOp blurOp = new ConvolveOp(blur, ConvolveOp.EDGE_NO_OP,
 				renderHint);
@@ -391,8 +445,7 @@
 		 */
 		int ncol = img.getColorModel().getNumColorComponents();
 		if ((ncol != 3) || (rgbm.length != 3) || (rgba.length != 3)) {
-			logger
-					.debug("ERROR(enhance): unknown number of color bands or coefficients ("
+			logger.debug("ERROR(enhance): unknown number of color bands or coefficients ("
 							+ ncol + ")");
 			return;
 		}
@@ -403,7 +456,7 @@
 
 	/**
 	 * Ensures that the array f is in the right order to map the images RGB
-	 * components. (not shure what happens
+	 * components. (not sure what happens otherwise)
 	 */
 	public float[] rgbOrdered(float[] fa) {
 		/*
@@ -443,6 +496,31 @@
 		return fb;
 	}
 
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see
+	 * digilib.image.DocuImageImpl#colorOp(digilib.image.DocuImage.ColorOps)
+	 */
+	public void colorOp(ColorOp op) throws ImageOpException {
+		if (op == ColorOp.GRAYSCALE) {
+			// convert image to grayscale
+			logger.debug("Color op: grayscaling");
+			ColorConvertOp colop = new ColorConvertOp(
+					ColorSpace.getInstance(ColorSpace.CS_GRAY), renderHint);
+			img = colop.filter(img, null);
+		} else if (op == ColorOp.INVERT) {
+			// invert colors i.e. invert every channel
+			logger.debug("Color op: inverting");
+			// TODO: is this enough for all image types?
+			LookupOp colop = new LookupOp(invertByteTable, renderHint);
+			ColorModel cm = img.getColorModel();
+			logger.debug("colop: image="+img+" colormodel="+cm);
+			img = colop.filter(img, null);
+		}
+
+	}
+
 	public void rotate(double angle) throws ImageOpException {
 		// setup rotation
 		double rangle = Math.toRadians(angle);
@@ -504,23 +582,8 @@
 		img = mirImg;
 	}
 
-	/*
-	 * (non-Javadoc)
-	 * 
-	 * @see java.lang.Object#finalize()
-	 */
-	protected void finalize() throws Throwable {
-		dispose();
-		super.finalize();
-	}
-
 	public void dispose() {
-		// we must dispose the ImageReader because it keeps the filehandle
-		// open!
-		if (reader != null) {
-			reader.dispose();
-			reader = null;
-		}
+	    // is this necessary?
 		img = null;
 	}
 
--- a/servlet/src/digilib/image/ImageSize.java	Tue Mar 01 17:12:25 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,230 +0,0 @@
-/*
- * ImageSize.java -- digilib image size class. 
- * Digital Image Library servlet components 
- * Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de) 
- * This program is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 2 of the License, or (at your option) any later
- * version. Please read license.txt for the full details. A copy of the GPL may
- * be found at http://www.gnu.org/copyleft/lgpl.html You should have received a
- * copy of the GNU General Public License along with this program; if not,
- * write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
- * Boston, MA 02111-1307 USA Created on 26.08.2003
- */
-
-package digilib.image;
-
-/** Class for image size (width, height).
- * 
- * A width or height of 0 is treated as a 'wildcard' that matches any size.
- * 
- * @author casties
- *          
- */
-public class ImageSize {
-	public int width;
-	public int height;
-
-	public ImageSize() {
-		super();
-	}
-
-	public ImageSize(int width, int height) {
-		this.width = width;
-		this.height = height;
-	}
-
-	public ImageSize(ImageSize i) {
-		this.width = i.width;
-		this.height = i.height;
-	}
-
-	public void setSize(int width, int height) {
-		this.width = width;
-		this.height = height;
-	}
-
-	/**
-	 * Returns if the size of this image is smaller in every dimension than the
-	 * other image.
-	 * 
-	 * 
-	 * 
-	 * @param is
-	 * @return
-	 */
-	public boolean isTotallySmallerThan(ImageSize is) {
-		if ((this.width == 0)||(is.width == 0)) {
-			// width wildcard
-			return (this.height <= is.height);
-		}
-		if ((this.height == 0)||(is.height == 0)) {
-			// height wildcard
-			return (this.width <= is.width);
-		}
-		return ((this.width <= is.width)&&(this.height <= is.height));
-	}
-
-	/**
-	 * Returns if the size of this image is smaller in at least one dimension
-	 * than the other image.
-	 * 
-	 * @param is
-	 * @return
-	 */
-	public boolean isSmallerThan(ImageSize is) {
-		if ((this.width == 0)||(is.width == 0)) {
-			// width wildcard
-			return (this.height <= is.height);
-		}
-		if ((this.height == 0)||(is.height == 0)) {
-			// height wildcard
-			return (this.width <= is.width);
-		}
-		return ((this.width <= is.width) || (this.height <= is.height));
-	}
-
-	/**
-	 * Returns if the size of this image is bigger in every dimension than the
-	 * other image.
-	 * 
-	 * 
-	 * 
-	 * @param is
-	 * @return
-	 */
-	public boolean isTotallyBiggerThan(ImageSize is) {
-		if ((this.width == 0)||(is.width == 0)) {
-			// width wildcard
-			return (this.height >= is.height);
-		}
-		if ((this.height == 0)||(is.height == 0)) {
-			// height wildcard
-			return (this.width >= is.width);
-		}
-		return ((this.width >= is.width) && (this.height >= is.height));
-	}
-
-	/**
-	 * Returns if the size of this image is bigger in at least one dimension
-	 * than the other image.
-	 * 
-	 * 
-	 * 
-	 * @param is
-	 * @return
-	 */
-	public boolean isBiggerThan(ImageSize is) {
-		if ((this.width == 0)||(is.width == 0)) {
-			// width wildcard
-			return (this.height >= is.height);
-		}
-		if ((this.height == 0)||(is.height == 0)) {
-			// height wildcard
-			return (this.width >= is.width);
-		}
-		return ((this.width >= is.width) || (this.height >= is.height));
-	}
-
-	/**
-	 * Returns if this image has the same size or height as the other image.
-	 * 
-	 * 
-	 * 
-	 * @param is
-	 * @return
-	 */
-	public boolean fitsIn(ImageSize is) {
-		if ((this.width == 0)||(is.width == 0)) {
-			// width wildcard
-			return (this.height == is.height);
-		}
-		if ((this.height == 0)||(is.height == 0)) {
-			// height wildcard
-			return (this.width == is.width);
-		}
-		return (
-			(this.width == is.width)
-				&& (this.height <= is.height)
-				|| (this.width <= is.width)
-				&& (this.height == is.height));
-	}
-
-	/**
-	 * Returns if the size of this image is the same as the other image.
-	 * 
-	 * 
-	 * 
-	 * @param is
-	 * @return
-	 */
-	public boolean equals(ImageSize is) {
-		if ((this.width == 0)||(is.width == 0)) {
-			// width wildcard
-			return (this.height == is.height);
-		}
-		if ((this.height == 0)||(is.height == 0)) {
-			// height wildcard
-			return (this.width == is.width);
-		}
-		return ((this.width == is.width) && (this.height == is.height));
-	}
-
-	/**
-	 * @return
-	 */
-	public int getHeight() {
-		return height;
-	}
-
-	/**
-	 * @param height
-	 */
-	public void setHeight(int height) {
-		this.height = height;
-	}
-
-	/**
-	 * @return
-	 */
-	public int getWidth() {
-		return width;
-	}
-
-	/**
-	 * @param width
-	 */
-	public void setWidth(int width) {
-		this.width = width;
-	}
-
-	/**
-	 * Returns the aspect ratio.
-	 * 
-	 * Aspect ratio is (width/height). So it's <1 for portrait and  >1 for
-	 * landscape.
-	 * 
-	 * @return
-	 */
-	public float getAspect() {
-		return (height > 0) ? ((float) width / (float) height) : 0;
-	}
-	
-	/**
-	 * Returns a scaled copy of this image size. 
-	 * 
-	 * @param scale
-	 * @return
-	 */
-	public ImageSize getScaled(float scale) {
-		return new ImageSize((int) (width * scale), (int) (height * scale));
-	}
-	
-	/* (non-Javadoc)
-	 * @see java.lang.Object#toString()
-	 */
-	public String toString() {
-		String s = "[" + width + "x" + height + "]";
-		return s;
-	}
-}
--- a/servlet/src/digilib/image/ImageWorker.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/image/ImageWorker.java	Tue Mar 01 22:00:50 2011 +0100
@@ -19,7 +19,6 @@
  */
 public class ImageWorker implements Callable<DocuImage> {
 
-    
     protected static Logger logger = Logger.getLogger(ImageWorker.class);
     private DigilibConfiguration dlConfig;
     private ImageJobDescription jobinfo;
@@ -69,19 +68,14 @@
                 logger.debug("Using subsampling: " + subsamp + " rest "
                         + scaleXY);
             }
-
-            docuImage.loadSubimage(jobinfo.getFileToLoad(), loadRect, (int) subsamp);
-
+            docuImage.loadSubimage(jobinfo.getInput(), loadRect, (int) subsamp);
             logger.debug("SUBSAMP: " + subsamp + " -> " + docuImage.getSize());
-
             docuImage.scale(scaleXY, scaleXY);
-
         } else {
             // else load and crop the whole file
-            docuImage.loadImage(jobinfo.getFileToLoad());
+            docuImage.loadImage(jobinfo.getInput());
             docuImage.crop((int) loadRect.getX(), (int) loadRect.getY(),
                     (int) loadRect.getWidth(), (int) loadRect.getHeight());
-
             docuImage.scale(scaleXY, scaleXY);
         }
 
@@ -144,6 +138,12 @@
             docuImage.enhance(mult, paramBRGT);
         }
 
+        // color operation
+        DocuImage.ColorOp colop = jobinfo.getColOp();
+        if (colop != null) {
+        	docuImage.colorOp(colop);
+        }
+        
         logger.debug("rendered in " + (System.currentTimeMillis() - startTime) + "ms");
 
         return docuImage;
--- a/servlet/src/digilib/image/JAIDocuImage.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/image/JAIDocuImage.java	Tue Mar 01 22:00:50 2011 +0100
@@ -24,7 +24,6 @@
 import java.awt.RenderingHints;
 import java.awt.image.RenderedImage;
 import java.awt.image.renderable.ParameterBlock;
-import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.ArrayList;
@@ -46,8 +45,8 @@
 
 import digilib.io.FileOpException;
 import digilib.io.FileOps;
-import digilib.io.ImageFile;
-import digilib.io.ImageFileset;
+import digilib.io.ImageInput;
+import digilib.util.ImageSize;
 
 /** A DocuImage implementation using Java Advanced Imaging Library. */
 /**
@@ -104,53 +103,64 @@
     }
 
 	/* Check image size and type and store in ImageFile f */
-	public ImageFile identify(ImageFile imageFile) throws IOException {
+	public ImageInput identify(ImageInput input) throws IOException {
+        this.input = input;
         // try parent method first
-	    ImageFile imf = super.identify(imageFile);
+	    ImageInput imf = super.identify(input);
 		if (imf != null) {
 			return imf;
 		}
-		// fileset to store the information
-		ImageFileset imgfs = imageFile.getParent();
-		File f = imageFile.getFile();
-		if (f == null) {
-			throw new IOException("File not found!");
-		}
 		/*
 		 * try JAI
 		 */
-		logger.debug("identifying (JAI) " + f);
+		logger.debug("identifying (JAI) " + input);
 		try {
-			RenderedOp img = JAI.create("fileload", f.getAbsolutePath());
+		    RenderedOp img = null;
+		    if (input.hasFile()) {
+		        String t = FileOps.mimeForFile(input.getFile());
+		        input.setMimetype(t);
+		        img = JAI.create("fileload", input.getFile().getAbsolutePath());
+		    } else if (input.hasInputStream()) {
+                img = JAI.create("stream", input.getInputStream());
+                // FIXME: where do we get the mimetype?
+		    } else {
+	            throw new FileOpException("unable to get data for image!");
+		    }
 			ImageSize d = new ImageSize(img.getWidth(), img.getHeight());
-			imageFile.setSize(d);
-			String t = FileOps.mimeForFile(f);
-			imageFile.setMimetype(t);
-			// logger.debug(" format:"+t);
-			if (imgfs != null) {
-				imgfs.setAspect(d);
-			}
-			logger.debug("image size: " + imageFile.getSize());
-			return imageFile;
+			input.setSize(d);
+			logger.debug("image size: " + d);
+			return input;
 		} catch (Exception e) {
-			throw new FileOpException("ERROR: unknown image file format!");
+			throw new FileOpException("ERROR: unable to identify image!");
 		}
 	}
 
 	/* Load an image file into the Object. */
-	public void loadImage(ImageFile f) throws FileOpException {
-		img = JAI.create("fileload", f.getFile().getAbsolutePath());
+	public void loadImage(ImageInput ii) throws FileOpException {
+        this.input = ii;
+        if (ii.hasFile()) {
+            img = JAI.create("fileload", ii.getFile().getAbsolutePath());
+        } else if (ii.hasInputStream()) {
+            img = JAI.create("stream", ii.getInputStream());
+        } else {
+            throw new FileOpException("unable to get data for image!");
+        }
 		if (img == null) {
 			throw new FileOpException("Unable to load File!");
 		}
-        mimeType = f.getMimetype();
 	}
 
 	/* Load an image file into the Object. */
-	public void loadSubimage(ImageFile f, Rectangle region, int subsample)
-			throws FileOpException {
+	public void loadSubimage(ImageInput ii, Rectangle region, int subsample) throws FileOpException {
 		logger.debug("loadSubimage");
-		img = JAI.create("fileload", f.getFile().getAbsolutePath());
+        this.input = ii;
+        if (ii.hasFile()) {
+            img = JAI.create("fileload", ii.getFile().getAbsolutePath());
+        } else if (ii.hasInputStream()) {
+            img = JAI.create("stream", ii.getInputStream());
+        } else {
+            throw new FileOpException("unable to get data for image!");
+        }
 		if ((region.width < img.getWidth())
 				|| (region.height < img.getHeight())) {
 			// setup Crop
@@ -174,7 +184,6 @@
 			// scale
 			logger.debug("loadSubimage: scale");
 			img = JAI.create("scale", sp);
-            mimeType = f.getMimetype();
 		}
 	}
 
--- a/servlet/src/digilib/image/JAIImageLoaderDocuImage.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/image/JAIImageLoaderDocuImage.java	Tue Mar 01 22:00:50 2011 +0100
@@ -38,7 +38,8 @@
 import javax.servlet.ServletException;
 
 import digilib.io.FileOpException;
-import digilib.io.ImageFile;
+import digilib.io.ImageInput;
+import digilib.util.ImageSize;
 
 /** DocuImage implementation using the Java Advanced Imaging API and the ImageLoader
  * API of Java 1.4.
@@ -80,44 +81,73 @@
 
 
 	/* Load an image file into the Object. */
-	public void loadImage(ImageFile f) throws FileOpException {
-		logger.debug("loadImage: "+f.getFile());
-		//System.gc();
-		img = JAI.create("ImageRead", f.getFile().getAbsolutePath());
+	public void loadImage(ImageInput ii) throws FileOpException {
+		logger.debug("loadImage: "+ii);
+		if (ii.hasImageInputStream()) {
+			img = JAI.create("ImageRead", ii.getImageInputStream());
+		} else if (ii.hasFile()) {
+			img = JAI.create("ImageRead", ii.getFile().getAbsolutePath());
+		}
 		if (img == null) {
 			throw new FileOpException("Unable to load File!");
 		}
-        mimeType = f.getMimetype();
 	}
 
 	/* Get an ImageReader for the image file. */
-	public ImageReader getReader(ImageFile f) throws IOException {
-		logger.debug("preloadImage: "+f.getFile());
-		//System.gc();
-		RandomAccessFile rf = new RandomAccessFile(f.getFile(), "r");
-		ImageInputStream istream = new FileImageInputStream(rf);
-		//Iterator readers = ImageIO.getImageReaders(istream);
-		Iterator<ImageReader> readers = ImageIO.getImageReadersByMIMEType(f.getMimetype());
-		if (! readers.hasNext()) {
-			throw new FileOpException("Unable to load File!");
-		}
-		reader = readers.next();
-		logger.debug("JAIImageIO: this reader: " + reader.getClass());
-		while (readers.hasNext()) {
-			logger.debug("  next reader: " + readers.next().getClass());
-		}
-		reader.setInput(istream);
-		return reader;
+	public ImageReader getReader(ImageInput input) throws IOException {
+        logger.debug("get ImageReader for " + input);
+        if (this.reader != null) {
+            if (this.input == input) {
+                // it was the same input
+                logger.debug("reusing Reader");
+                return reader;
+            }
+            // clean up old reader
+            logger.debug("cleaning Reader!");
+            dispose();
+        }
+        this.input = input;
+        ImageInputStream istream = null;
+        if (input.hasImageInputStream()) {
+            // stream input
+            istream = input.getImageInputStream();
+        } else if (input.hasFile()) {
+            // file only input
+            RandomAccessFile rf = new RandomAccessFile(input.getFile(), "r");
+            istream = new FileImageInputStream(rf);
+        } else {
+            throw new FileOpException("Unable to get data from ImageInput");
+        }
+        Iterator<ImageReader> readers;
+        String mt = input.getMimetype();
+        if (mt == null) {
+            logger.debug("No mime-type. Trying automagic.");
+            readers = ImageIO.getImageReaders(istream);
+        } else {
+            logger.debug("File type:" + mt);
+            readers = ImageIO.getImageReadersByMIMEType(mt);
+        }
+        if (!readers.hasNext()) {
+            throw new FileOpException("Can't find Reader to load File!");
+        }
+        reader = readers.next();
+        logger.debug("ImageIO: this reader: " + reader.getClass());
+        /* are there more readers? */
+        /* while (readers.hasNext()) {
+            logger.debug("ImageIO: next reader: " + readers.next().getClass());
+        } */
+        reader.setInput(istream);
+        return reader;
 	}
 
 	/* Load an image file into the Object. */
-	public void loadSubimage(ImageFile f, Rectangle region, int prescale)
+	public void loadSubimage(ImageInput ii, Rectangle region, int prescale)
 		throws FileOpException {
-		logger.debug("loadSubimage: "+f.getFile());
+		logger.debug("loadSubimage: "+ii.getFile());
 		//System.gc();
 		try {
-			if ((reader == null) || (imgFile != f.getFile())) {
-				getReader(f);
+			if ((reader == null) || (imgFile != ii.getFile())) {
+				getReader(ii);
 			}
 			ImageReadParam readParam = reader.getDefaultReadParam();
 			readParam.setSourceRegion(region);
@@ -137,8 +167,7 @@
 		if (img == null) {
 			throw new FileOpException("Unable to load File!");
 		}
-		imgFile = f.getFile();
-        mimeType = f.getMimetype();
+		imgFile = ii.getFile();
 	}
 
 
--- a/servlet/src/digilib/io/AliasingDocuDirCache.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/io/AliasingDocuDirCache.java	Tue Mar 01 22:00:50 2011 +0100
@@ -28,6 +28,7 @@
 
 import digilib.io.FileOps.FileClass;
 import digilib.servlet.DigilibConfiguration;
+import digilib.util.XMLListLoader;
 
 /**
  * @author casties
--- a/servlet/src/digilib/io/DigilibInfoReader.java	Tue Mar 01 17:12:25 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-package digilib.io;
-
-/** DigilibInfoReader 
- * A class for reading the information from info.xml files used in digilib image directories.
- *
- */
-
-import java.io.File;
-import java.util.List;
-
-import org.apache.log4j.Logger;
-import org.jdom.Document;
-import org.jdom.Element;
-import org.jdom.input.SAXBuilder;
-
-
-
-public class DigilibInfoReader {
-
-	/** gengeral logger for this class */
-	protected static Logger logger = Logger.getLogger("digilib.servlet");
-	
-	private String filename = null;
-	//private static String base_element = "info";
-	
-	public DigilibInfoReader(String fn){
-		filename = fn;
-	}
-
-	/**
-	 * Returns the attribute defined by 'attr' as a String.
-	 * 
-	 * @param attr
-	 * @return
-	 */
-	@SuppressWarnings("unchecked") // Element.getChildren() returns naked List
-    public String getAsString(String attr){
-		try{
-			SAXBuilder builder = new SAXBuilder();
-			Document doc = builder.build(new File(filename));
-			Element root = doc.getRootElement();
-			List<Element> mainElements = root.getChildren();
-			// logger.debug("XML mainElements:"+mainElements.toString());
-
-			for(int i=0; i<mainElements.size(); i++){
-				Element elem = mainElements.get(i);
-				if(elem.getName()==attr){
-					// logger.debug(attr+" == "+(String)elem.getTextTrim());
-					return (String)elem.getTextTrim();
-				}
-			}
-
-		}
-		catch(Exception e){
-			logger.error(e.getMessage());
-		}
-		return null;
-	}
-	
-	
-	/**
-	 * Find out if the info.xml exists
-	 * @return
-	 */
-	public boolean hasInfo(){
-		try {
-			SAXBuilder builder = new SAXBuilder();
-			builder.build(new File(filename));
-			return true;
-		}
-		catch(Exception e){
-			return false;
-		}
-	}
-	
-}
--- a/servlet/src/digilib/io/DocuDirCache.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/io/DocuDirCache.java	Tue Mar 01 22:00:50 2011 +0100
@@ -23,10 +23,10 @@
 package digilib.io;
 
 import java.io.File;
-import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 
 import org.apache.log4j.Logger;
 
@@ -42,7 +42,7 @@
 	Logger logger = Logger.getLogger(this.getClass());
 
 	/** HashMap of directories */
-	Map<String, DocuDirectory> map = null;
+	ConcurrentMap<String, DocuDirectory> map = new ConcurrentHashMap<String, DocuDirectory>();
 
 	/** names of base directories */
 	String[] baseDirNames = null;
@@ -71,7 +71,6 @@
 	public DocuDirCache(String[] bd, FileClass[] fcs,
 			DigilibConfiguration dlConfig) {
 		baseDirNames = bd;
-		map = new HashMap<String, DocuDirectory>();
 		this.fileClasses = fcs;
 	}
 
@@ -83,7 +82,6 @@
 	 */
 	public DocuDirCache(String[] bd) {
 		baseDirNames = bd;
-		map = new HashMap<String, DocuDirectory>();
 		// default file class is CLASS_IMAGE
 		fileClasses = new FileClass[] { FileClass.IMAGE };
 	}
@@ -99,38 +97,49 @@
 
 	/**
 	 * Add a DocuDirectory to the cache.
+	 * Always returns the correct Object from the cache, 
+	 * either newdir one or another one.
 	 * 
 	 * @param newdir
+	 * @return dir
 	 */
-	public void put(DocuDirectory newdir) {
+	public DocuDirectory put(DocuDirectory newdir) {
 		String s = newdir.getDirName();
 		logger.debug("DocuDirCache.put for "+s+" in "+this);
-		if (map.containsKey(s)) {
+		DocuDirectory olddir = map.putIfAbsent(s, newdir);
+		if (olddir != null) {
 			logger.warn("Duplicate key in DocuDirCache.put -- ignoring!");
-		} else {
-			map.put(s, newdir);
-			numFiles += newdir.size();
+			return olddir;
 		}
+		numFiles += newdir.size();
+		return newdir;
 	}
 
 	/**
 	 * Add a directory to the cache and check its parents.
-	 * 
+	 * Always returns the correct Object from the cache, 
+	 * either newDir one or another one.
+	 *
 	 * @param newDir
+	 * @return dir
 	 */
-	public void putDir(DocuDirectory newDir) {
-		put(newDir);
-		String parent = FileOps.parent(newDir.getDirName());
-		if (parent != "") {
-			// check the parent in the cache
-			DocuDirectory pd = map.get(parent);
-			if (pd == null) {
-				// the parent is unknown
-				pd = new DocuDirectory(parent, this);
-				putDir(pd);
+	public DocuDirectory putDir(DocuDirectory newDir) {
+		DocuDirectory dd = put(newDir);
+		if (dd.getParent() == null) {
+			// no parent link yet
+			String parent = FileOps.parent(newDir.getDirName());
+			if (parent != "") {
+				// check the parent in the cache
+				DocuDirectory pd = map.get(parent);
+				if (pd == null) {
+					// the parent is unknown
+					pd = new DocuDirectory(parent, this);
+					pd = putDir(pd);
+				}
+				newDir.setParent(pd);
 			}
-			newDir.setParent(pd);
 		}
+		return dd;
 	}
 
 	/**
@@ -182,8 +191,6 @@
 		int n = in - 1;
 		// first, assume fn is a directory and look in the cache
 		dd = map.get(fn);
-        // logger.debug("fn: " + fn);
-        // logger.debug("dd: " + dd);
 		if (dd == null) {
 			// cache miss
 			misses++;
@@ -196,7 +203,7 @@
 				dd = new DocuDirectory(fn, this);
 				if (dd.isValid()) {
 					// add to the cache
-					putDir(dd);
+					dd = putDir(dd);
 				}
 			} else {
 				/*
@@ -214,7 +221,7 @@
 					if (dd.isValid()) {
 						// add to the cache
                         // logger.debug(dd + " is valid");
-						putDir(dd);
+						dd = putDir(dd);
 					} else {
 						// invalid path
 						return null;
@@ -225,17 +232,14 @@
 				}
 				// get the file's index
 				n = dd.indexOf(f.getName(), fc);
-                // logger.debug(f.getName() + ", index is " + n + ", fc = " + fc);
 			}
 		} else {
 			// cache hit
 			hits++;
 		}
 		dd.refresh();
-        // logger.debug(dd + " refreshed");
 		if (dd.isValid()) {
 			try {
-                // logger.debug(dd + " is valid");
 				return dd.get(n, fc);
 			} catch (IndexOutOfBoundsException e) {
                 // logger.debug(fn + " not found in directory");
@@ -266,7 +270,7 @@
 				dd = new DocuDirectory(fn, this);
 				if (dd.isValid()) {
 					// add to the cache
-					putDir(dd);
+					dd = putDir(dd);
 				}
 			} else {
 				// maybe it's a file
@@ -278,7 +282,7 @@
 						dd = new DocuDirectory(f.getParent(), this);
 						if (dd.isValid()) {
 							// add to the cache
-							putDir(dd);
+							dd = putDir(dd);
 						} else {
 							// invalid path
 							return null;
--- a/servlet/src/digilib/io/DocuDirectory.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/io/DocuDirectory.java	Tue Mar 01 22:00:50 2011 +0100
@@ -31,6 +31,8 @@
 import org.xml.sax.SAXException;
 
 import digilib.io.FileOps.FileClass;
+import digilib.meta.MetadataMap;
+import digilib.meta.XMLMetaLoader;
 
 /**
  * @author casties
@@ -40,9 +42,6 @@
 	/** list of files (DocuDirent) */
 	private List<List<DocuDirent>> list = null;
 
-	/** default FileClass for unspecified calls */
-	public static FileClass defaultFileClass = FileClass.IMAGE;
-	
 	/** directory object is valid (exists on disk) */
 	private boolean isValid = false;
 
@@ -52,6 +51,9 @@
 	/** directory name (digilib canonical form) */
 	private String dirName = null;
 
+	/** array of parallel dirs for scaled images */
+	private Directory[] dirs = null;
+
 	/** directory metadata */
 	private MetadataMap dirMeta = null;
 
@@ -83,15 +85,6 @@
 	public DocuDirectory(String path, DocuDirCache cache) {
 		this.dirName = path;
 		this.cache = cache;
-		initDir();
-		checkDir();
-	}
-
-	/**
-	 * Sets and checks the dir object.
-	 *  
-	 */
-	protected void initDir() {
 		String baseDirName = cache.getBaseDirNames()[0];
 		// clear directory list
 		FileClass[] fcs = FileClass.values();
@@ -100,10 +93,10 @@
 		for (@SuppressWarnings("unused") FileClass fc: fcs) {
 		    list.add(null);
 		}
-		isValid = false;
 		dirMTime = 0;
 		// the first directory has to exist
-		dir = new File(baseDirName, dirName);
+		dir = new File(baseDirName, path);
+		isValid = dir.isDirectory();
 	}
 
 	/**
@@ -111,7 +104,7 @@
 	 *  
 	 */
 	public int size() {
-	    return size(defaultFileClass);
+		return ((list != null) && (list.get(0) != null)) ? list.get(0).size() : 0;
 	}
 
 	/**
@@ -121,13 +114,7 @@
 	 *            fileClass
 	 */
 	public int size(FileClass fc) {
-        if (list != null) {
-            List<DocuDirent> l = list.get(fc.ordinal());
-            if (l != null) {
-                return l.size();
-            }
-        }
-        return 0;
+		return ((list != null) && (list.get(fc.ordinal()) != null)) ? list.get(fc.ordinal()).size() : 0;
 	}
 
 	/**
@@ -136,8 +123,11 @@
 	 * @param index
 	 * @return
 	 */
-	public ImageFileset get(int index) {
-		return (ImageFileset) get(index, defaultFileClass);
+	public DocuDirent get(int index) {
+		if ((list == null) || (list.get(0) == null) || (index >= list.get(0).size())) {
+			return null;
+		}
+		return list.get(0).get(index);
 	}
 
 	/**
@@ -156,102 +146,84 @@
 	}
 
 	/**
-	 * Checks if the directory exists on the filesystem.
-	 * 
-	 * Sets isValid.
-	 * 
-	 * @return
-	 */
-	public boolean checkDir() {
-		if (dir == null) {
-			initDir();
-		}
-		isValid = dir.isDirectory();
-		return isValid;
-	}
-
-	/**
 	 * Read the filesystem directory and fill this object.
 	 * 
 	 * Clears the List and (re)reads all files.
 	 * 
 	 * @return boolean the directory exists
 	 */
-	public boolean readDir() {
+	public synchronized boolean readDir() {
 		// check directory first
-		checkDir();
 		if (!isValid) {
 			return false;
 		}
-		// first file extension to try for scaled directories
-		String scalext = null;
+		// re-check modification time because the thread may have slept
+		if (dir.lastModified() <= dirMTime) {
+			return true;
+		}
 		// read all filenames
-		logger.debug("reading directory " + dir.getPath());
+		logger.debug("reading directory "+this+" = "+dir.getPath());
+		File[] allFiles = null;
 		/*
 		 * using ReadableFileFilter is safer (we won't get directories with file
 		 * extensions) but slower.
 		 */
-		File[] allFiles = null;
-		//	allFiles = dir.listFiles(new FileOps.ReadableFileFilter());
+		// allFiles = dir.listFiles(new FileOps.ReadableFileFilter());
 		allFiles = dir.listFiles();
-		//logger.debug("  done");
 		if (allFiles == null) {
 			// not a directory
 			return false;
 		}
-		// list of base dirs from the parent cache
-		String[] baseDirNames = cache.getBaseDirNames();
-		// number of base dirs
-		int nb = baseDirNames.length;
-		// array of base dirs
-		Directory[] dirs = new Directory[nb];
-		// first entry is this directory
-		dirs[0] = this;
-		// fill array with the remaining directories
-		for (int j = 1; j < nb; j++) {
-			File d = new File(baseDirNames[j], dirName);
-			if (d.isDirectory()) {
-				dirs[j] = new Directory(d);
-				logger.debug("  reading scaled directory " + d.getPath());
-				dirs[j].readDir();
-				//logger.debug("    done");
+		// init parallel directories
+		if (dirs == null) {
+			// list of base dirs from the parent cache
+			String[] baseDirNames = cache.getBaseDirNames();
+			// number of base dirs
+			int nb = baseDirNames.length;
+			// array of parallel dirs
+			dirs = new Directory[nb];
+			// first entry is this directory
+			dirs[0] = this;
+			// fill array with the remaining directories
+			for (int j = 1; j < nb; j++) {
+				// add dirName to baseDirName
+				File d = new File(baseDirNames[j], dirName);
+				if (d.isDirectory()) {
+					dirs[j] = new Directory(d);
+					logger.debug("  reading scaled directory " + d.getPath());
+					dirs[j].readDir();
+				}
 			}
 		}
 
 		// go through all file classes
-        //for (int classIdx = 0; classIdx < FileOps.NUM_CLASSES; classIdx++) {
-		for (FileClass fileClass: cache.getFileClasses()) {
-			//fileClass = cache.getFileClasses()[classIdx];
-			File[] fileList = FileOps.listFiles(allFiles, FileOps
-					.filterForClass(fileClass));
-			//logger.debug(" done");
+		for (FileClass fileClass : cache.getFileClasses()) {
+			File[] fileList = FileOps.listFiles(allFiles,
+					FileOps.filterForClass(fileClass));
 			// number of files in the directory
 			int numFiles = fileList.length;
 			if (numFiles > 0) {
 				// create new list
-				list.set(fileClass.ordinal(), new ArrayList<DocuDirent>(numFiles));
-				// sort the file names alphabetically and iterate the list
-				// Arrays.sort(fileList); // not needed <hertzhaft>
-				Map<Integer, Object> hints = FileOps.newHints(FileOps.HINT_BASEDIRS, dirs);
-				hints.put(FileOps.HINT_FILEEXT, scalext);
-				for (int i = 0; i < numFiles; i++) {
-					DocuDirent f = FileOps.fileForClass(fileClass, fileList[i],
-							hints);
+				ArrayList<DocuDirent> dl = new ArrayList<DocuDirent>(numFiles);
+				list.set(fileClass.ordinal(), dl);
+				for (File f : fileList) {
+					DocuDirent df = FileOps.fileForClass(fileClass, f, dirs);
+					df.setParent(this);
 					// add the file to our list
-                    // logger.debug(f.getName());
-
-					list.get(fileClass.ordinal()).add(f);
-					f.setParent(this);
+					dl.add(df);
 				}
-                // we sort the inner ArrayList (the list of files not the list of file types)
-				// for binarySearch to work (DocuDirent's natural sort order is by filename)
-                Collections.sort(list.get(fileClass.ordinal()));
+				/*
+				 * we sort the inner ArrayList (the list of files not the list
+				 * of file types) for binarySearch to work (DocuDirent's natural
+				 * sort order is by filename)
+				 */
+				Collections.sort(dl);
 			}
 		}
 		// clear the scaled directories
-		for (int j = 1; j < nb; j++) {
-			if (dirs[j] != null) {
-				dirs[j].clearFilenames();
+		for (Directory d: dirs) {
+			if (d != null) {
+				d.clearFilenames();
 			}
 		}
 		// update number of cached files if this was the first time
@@ -456,9 +428,8 @@
 		return -1;
 	}
 
-	private boolean isBasenameInList(List<DocuDirent> fl, int idx, String fn) {
-		String dfn = FileOps.basename((fl.get(idx))
-				.getName());
+	private boolean isBasenameInList(List<DocuDirent> fileList, int idx, String fn) {
+		String dfn = FileOps.basename((fileList.get(idx)).getName());
 		return (dfn.equals(fn)||dfn.equals(FileOps.basename(fn))); 
 	}
 	
@@ -474,7 +445,12 @@
 	 * @return DocuDirent
 	 */
 	public DocuDirent find(String fn) {
-		return find(fn, defaultFileClass);
+		FileClass fc = FileOps.classForFilename(fn);
+		int i = indexOf(fn, fc);
+		if (i >= 0) {
+			return list.get(0).get(i);
+		}
+		return null;
 	}
 
 	/**
@@ -491,7 +467,7 @@
 	public DocuDirent find(String fn, FileClass fc) {
 		int i = indexOf(fn, fc);
 		if (i >= 0) {
-			return (DocuDirent) list.get(fc.ordinal()).get(i);
+			return list.get(fc.ordinal()).get(i);
 		}
 		return null;
 	}
--- a/servlet/src/digilib/io/DocuDirent.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/io/DocuDirent.java	Tue Mar 01 22:00:50 2011 +0100
@@ -1,169 +1,81 @@
-/*
- * DocuDirent.java -- Abstract directory entry in a DocuDirectory
- * 
- * Digital Image Library servlet components
- * 
- * Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)
- * 
- * This program is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- * 
- * Please read license.txt for the full details. A copy of the GPL may be found
- * at http://www.gnu.org/copyleft/lgpl.html
- * 
- * You should have received a copy of the GNU General Public License along with
- * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
- * Place, Suite 330, Boston, MA 02111-1307 USA
- * 
- * Created on 15.09.2003 by casties
- *  
- */
 package digilib.io;
 
 import java.io.File;
-import java.util.Map;
-
-import org.apache.log4j.Logger;
-
-import digilib.io.FileOps.FileClass;
 
-/**
- * Abstract directory entry in a DocuDirectory.
- * 
- * @author casties
- *  
- */
-public abstract class DocuDirent implements Comparable<Object> {
+import digilib.meta.MetadataMap;
+
+public interface DocuDirent extends Comparable<Object> {
 
-	/** the file class of this file */
-	protected static FileClass fileClass = FileClass.NONE;
-	/** HashMap with metadata */
-	protected MetadataMap fileMeta = null;
-	/** Is the Metadata valid */
-	protected boolean metaChecked = false;
-	/** the parent directory */
-	protected Directory parent = null;
-
-	/**
-	 * Checks metadata and does something with it.
-	 *  
-	 */
-	public abstract void checkMeta();
+    /**
+     * Checks metadata and does something with it.
+     *  
+     */
+    public abstract void checkMeta();
 
-	/**
-	 * gets the (default) File
-	 * 
-	 * @return
-	 */
-	public abstract File getFile();
+    /**
+     * gets the (default) File
+     * 
+     * @return
+     */
+    public abstract File getFile();
 
-	/**
-	 * Reads meta-data for this Fileset if there is any.
-	 *  
-	 */
-	public void readMeta() {
-		if ((fileMeta != null) || (getFile() == null)) {
-			// there is already metadata or there is no file
-			return;
-		}
-		// metadata is in the file {filename}.meta
-		String fn = getFile().getAbsolutePath();
-		File mf = new File(fn + ".meta");
-		if (mf.canRead()) {
-			XMLMetaLoader ml = new XMLMetaLoader();
-			try {
-				// read meta file
-				Map<String, MetadataMap> meta = ml.loadURL(mf.getAbsolutePath());
-				if (meta == null) {
-					return;
-				}
-				fileMeta = meta.get(getName());
-			} catch (Exception e) {
-				Logger.getLogger(this.getClass()).warn("error reading file .meta", e);
-			}
-		}
-	}
+    /**
+     * Reads meta-data for this Fileset if there is any.
+     *  
+     */
+    public abstract void readMeta();
+
+    /**
+     * The name of the file.
+     * 
+     * If this is a Fileset, the method returns the name of the default file
+     * (for image filesets the highest resolution file).
+     * 
+     * @return
+     */
+    public abstract String getName();
 
-	/**
-	 * The name of the file.
-	 * 
-	 * If this is a Fileset, the method returns the name of the default file
-	 * (for image filesets the highest resolution file).
-	 * 
-	 * @return
-	 */
-	public String getName() {
-		File f = getFile();
-		return (f != null) ? f.getName() : null;
-	} 
-	
-	/**
-	 * Returns the parent Directory.
-	 * 
-	 * @return DocuDirectory
-	 */
-	public Directory getParent() {
-		return parent;
-	}
-	
-	/**
-	 * Sets the parent Directory.
-	 * 
-	 * @param parent
-	 *            The parent to set
-	 */
-	public void setParent(Directory parent) {
-		this.parent = parent;
-	} 
-	
-	/**
-	 * Returns the meta-data for this file(set).
-	 * 
-	 * @return HashMap
-	 */
-	public MetadataMap getFileMeta() {
-		return fileMeta;
-	} 
-	
-	/**
-	 * Sets the meta-data for this file(set) .
-	 * 
-	 * @param fileMeta
-	 *            The fileMeta to set
-	 */
-	public void setFileMeta(MetadataMap fileMeta) {
-		this.fileMeta = fileMeta;
-	} 
-	
-	/**
-	 * @return
-	 */
-	public boolean isMetaChecked() {
-		return metaChecked;
-	} 
-	
-	/**
-	 * @return
-	 */
-	public static FileClass getFileClass() {
-		return fileClass;
-	}
+    /**
+     * Returns the parent Directory.
+     * 
+     * @return DocuDirectory
+     */
+    public abstract Directory getParent();
+
+    /**
+     * Sets the parent Directory.
+     * 
+     * @param parent
+     *            The parent to set
+     */
+    public abstract void setParent(Directory parent);
+
+    /**
+     * Returns the meta-data for this file(set).
+     * 
+     * @return HashMap
+     */
+    public abstract MetadataMap getFileMeta();
 
-	/** Comparator using the file name.
-	 * Compares to a String (for binarySearch)
+    /**
+     * Sets the meta-data for this file(set) .
+     * 
+     * @param fileMeta
+     *            The fileMeta to set
+     */
+    public abstract void setFileMeta(MetadataMap fileMeta);
+
+    /**
+     * @return
+     */
+    public abstract boolean isMetaChecked();
+
+    /** Comparator using the file name.
+     * Compares to a String (for binarySearch)
      * or to another DocuDirent (for sort)
-	 * 
-	 * @see java.lang.Comparable#compareTo(java.lang.Object)
-	 */
-	public int compareTo(Object arg0) {
-		if (arg0 instanceof DocuDirent) {
-		    return getName().compareTo(((DocuDirent) arg0).getName());
-		} else {
-		    return getName().compareTo((String) arg0);
-		}
-	}
+     * 
+     * @see java.lang.Comparable#compareTo(java.lang.Object)
+     */
+    public abstract int compareTo(Object arg0);
 
-	
-}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/DocuDirentImpl.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,149 @@
+/*
+ * DocuDirent.java -- Abstract directory entry in a DocuDirectory
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)
+ * 
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ * 
+ * Please read license.txt for the full details. A copy of the GPL may be found
+ * at http://www.gnu.org/copyleft/lgpl.html
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ * 
+ * Created on 15.09.2003 by casties
+ *  
+ */
+package digilib.io;
+
+import java.io.File;
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+
+import digilib.io.FileOps.FileClass;
+import digilib.meta.MetadataMap;
+import digilib.meta.XMLMetaLoader;
+
+/**
+ * Abstract directory entry in a DocuDirectory.
+ * 
+ * @author casties
+ *  
+ */
+public abstract class DocuDirentImpl implements DocuDirent {
+
+	/** the file class of this file */
+	protected static FileClass fileClass = FileClass.NONE;
+	/** HashMap with metadata */
+	protected MetadataMap fileMeta = null;
+	/** Is the Metadata valid */
+	protected boolean metaChecked = false;
+	/** the parent directory */
+	protected Directory parent = null;
+
+	/* (non-Javadoc)
+     * @see digilib.io.DocuDirent#checkMeta()
+     */
+	public abstract void checkMeta();
+
+	/* (non-Javadoc)
+     * @see digilib.io.DocuDirent#getInput()
+     */
+	public abstract File getFile();
+
+	/* (non-Javadoc)
+     * @see digilib.io.DocuDirent#readMeta()
+     */
+	public void readMeta() {
+		if ((fileMeta != null) || (getFile() == null)) {
+			// there is already metadata or there is no file
+			return;
+		}
+		// metadata is in the file {filename}.meta
+		String fn = getFile().getAbsolutePath();
+		File mf = new File(fn + ".meta");
+		if (mf.canRead()) {
+			XMLMetaLoader ml = new XMLMetaLoader();
+			try {
+				// read meta file
+				Map<String, MetadataMap> meta = ml.loadURL(mf.getAbsolutePath());
+				if (meta == null) {
+					return;
+				}
+				fileMeta = meta.get(getName());
+			} catch (Exception e) {
+				Logger.getLogger(this.getClass()).warn("error reading file .meta", e);
+			}
+		}
+	}
+
+	/* (non-Javadoc)
+     * @see digilib.io.DocuDirent#getName()
+     */
+	public String getName() {
+		File f = getFile();
+		return (f != null) ? f.getName() : null;
+	} 
+	
+	/* (non-Javadoc)
+     * @see digilib.io.DocuDirent#getParent()
+     */
+	public Directory getParent() {
+		return parent;
+	}
+	
+	/* (non-Javadoc)
+     * @see digilib.io.DocuDirent#setParent(digilib.io.Directory)
+     */
+	public void setParent(Directory parent) {
+		this.parent = parent;
+	} 
+	
+	/* (non-Javadoc)
+     * @see digilib.io.DocuDirent#getFileMeta()
+     */
+	public MetadataMap getFileMeta() {
+		return fileMeta;
+	} 
+	
+	/* (non-Javadoc)
+     * @see digilib.io.DocuDirent#setFileMeta(digilib.io.MetadataMap)
+     */
+	public void setFileMeta(MetadataMap fileMeta) {
+		this.fileMeta = fileMeta;
+	} 
+	
+	/* (non-Javadoc)
+     * @see digilib.io.DocuDirent#isMetaChecked()
+     */
+	public boolean isMetaChecked() {
+		return metaChecked;
+	} 
+	
+	/**
+	 * @return
+	 */
+	public static FileClass getFileClass() {
+		return fileClass;
+	}
+
+	/* (non-Javadoc)
+     * @see digilib.io.DocuDirent#compareTo(java.lang.Object)
+     */
+	public int compareTo(Object arg0) {
+		if (arg0 instanceof DocuDirentImpl) {
+		    return getName().compareTo(((DocuDirent) arg0).getName());
+		} else {
+		    return getName().compareTo((String) arg0);
+		}
+	}
+
+	
+}
--- a/servlet/src/digilib/io/FileOps.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/io/FileOps.java	Tue Mar 01 22:00:50 2011 +0100
@@ -107,7 +107,10 @@
 	 * get the mime type for a file format (by extension)
 	 */
 	public static String mimeForFile(File f) {
-		return (String) fileTypes.get(extname(f.getName().toLowerCase()));
+	    if (f == null) {
+	        return null;
+	    }
+		return fileTypes.get(extname(f.getName().toLowerCase()));
 	}
 
 	/**
@@ -329,20 +332,20 @@
 	/**
 	 * Factory for DocuDirents based on file class.
 	 * 
-	 * Returns an ImageFileset, TextFile or SVGFile. baseDirs and scalext are
+	 * Returns an ImageSet, TextFile or SVGFile. scaleDirs are
 	 * only for ImageFilesets.
 	 * 
 	 * @param fileClass
 	 * @param file
-	 * @param hints
+	 * @param scaleDirs
 	 *            optional additional parameters
 	 * @return
 	 */
-	public static DocuDirent fileForClass(FileClass fileClass, File file, Map<Integer,Object> hints) {
+	public static DocuDirent fileForClass(FileClass fileClass, File file, Directory[] scaleDirs) {
 		// what class of file do we have?
 		if (fileClass == FileClass.IMAGE) {
 			// image file
-			return new ImageFileset(file, hints);
+			return new ImageFileSet(file, scaleDirs);
 		} else if (fileClass == FileClass.TEXT) {
 			// text file
 			return new TextFile(file);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/ImageCacheStream.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,49 @@
+/**
+ * 
+ */
+package digilib.io;
+
+import java.io.InputStream;
+
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.MemoryCacheImageInputStream;
+
+/**
+ * @author casties
+ *
+ */
+public class ImageCacheStream extends ImageStream {
+
+    public ImageCacheStream(InputStream stream, String mimeType) {
+        super(stream, mimeType);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see digilib.io.ImageInput#hasImageInputStream()
+     */
+    @Override
+    public boolean hasImageInputStream() {
+        return true;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see digilib.io.ImageInput#getImageInputStream()
+     */
+    @Override
+    public ImageInputStream getImageInputStream() {
+        /*
+         * TODO: which type of stream backing? 
+         * In general, it is preferable to
+         * use a FileCacheImageInputStream when reading from a regular
+         * InputStream. This class is provided for cases where it is not
+         * possible to create a writable temporary file.
+         */
+        ImageInputStream iis = new MemoryCacheImageInputStream(this.stream);
+        return iis;
+    }
+
+}
--- a/servlet/src/digilib/io/ImageFile.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/io/ImageFile.java	Tue Mar 01 22:00:50 2011 +0100
@@ -22,74 +22,142 @@
 package digilib.io;
 
 import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
 
-import digilib.image.ImageSize;
+import javax.imageio.stream.FileImageInputStream;
+import javax.imageio.stream.ImageInputStream;
+
+import digilib.servlet.DigilibConfiguration;
+import digilib.util.ImageSize;
 
 /**
  * @author casties
  */
-public class ImageFile {
+public class ImageFile extends ImageInput {
 	
+	// file
+	private File file = null;
 	// file name
-	private String filename = null;
-	// parent ImageFileset
-	private ImageFileset parent = null;
+	private String name = null;
 	// parent directory
 	private Directory dir = null;
-	// mime file type
-	private String mimetype = null;
-	// image size in pixels
-	private ImageSize pixelSize = null;
 
-	public ImageFile(String fn, ImageFileset parent, Directory dir) {
-		this.filename = fn;
+	/** Constructor with File.
+	 * 
+	 * @param f
+	 * @param parent
+	 * @param dir
+	 */
+	public ImageFile(File f, ImageSet parent, Directory dir) {
+		this.file = f;
+		this.name = f.getName();
 		this.parent = parent;
 		this.dir = dir;
 	}
 	
-	public ImageFile(String fn) {
-		File f = new File(fn);
-		this.dir = new Directory(f.getParentFile());
-		this.filename = f.getName();
+	/** Constructor with filename (without path).
+	 * @param fn
+	 * @param parent
+	 * @param dir
+	 */
+	public ImageFile(String fn, ImageSet parent, Directory dir) {
+		this.name = fn;
+		this.dir = dir;
+		this.file = new File(this.dir.getDir(), fn);
+		this.parent = parent;
+	}
+	
+	
+	/** Checks the image and sets size and type.
+	 * 
+	 */
+	public void check() {
+	    if (pixelSize == null) {
+	        try {
+	            // use the configured toolkit to identify the image
+                DigilibConfiguration.identifyDocuImage(this);
+            } catch (IOException e) {
+                // nothing much to do...
+            }
+	    }
 	}
 	
-	/** Returns the file name (without path).
+	/* (non-Javadoc)
+     * @see digilib.io.ImageInput#getSize()
+     */
+    @Override
+    public ImageSize getSize() {
+        check();
+        return pixelSize;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.ImageInput#getMimetype()
+     */
+    @Override
+    public String getMimetype() {
+        check();
+        return mimetype;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.ImageInput#getAspect()
+     */
+    @Override
+    public float getAspect() {
+        check();
+        return (pixelSize != null) ? pixelSize.getAspect() : 0f;
+    }
+
+    /** Returns the file name (without path).
 	 * 
 	 * @return
 	 */
 	public String getName() {
-		return filename;
-	}
-
-
-	/**
-	 * @return File
-	 */
-	public File getFile() {
-		if (dir == null) {
-			return null;
-		}
-		File f = new File(dir.getDir(), filename);
-		return f;
+		return name;
 	}
 
-	/**
-	 * @return ImageSize
+	
+	/* (non-Javadoc)
+     * @see digilib.io.ImageInput#hasImageInputStream()
+     */
+    @Override
+    public boolean hasImageInputStream() {
+        return true;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.ImageInput#getImageInputStream()
+     */
+    @Override
+    public ImageInputStream getImageInputStream() {
+        try {
+            RandomAccessFile rf = new RandomAccessFile(file, "r");
+            return new FileImageInputStream(rf);
+        } catch (IOException e) {
+            // what now?
+        }
+        return null;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.ImageInput#hasFile()
+     */
+    @Override
+    public boolean hasFile() {
+        return true;
+    }
+
+    /* (non-Javadoc)
+	 * @see digilib.io.ImageInput#getFile()
 	 */
-	public ImageSize getSize() {
-		return pixelSize;
+	public File getFile() {
+		return file;
 	}
 
-	/**
-	 * @return String
-	 */
-	public String getMimetype() {
-		return mimetype;
-	}
-
-	/**
-	 * Sets the imageSize.
-	 * @param imageSize The imageSize to set
+	/* (non-Javadoc)
+	 * @see digilib.io.ImageInput#setSize(digilib.image.ImageSize)
 	 */
 	public void setSize(ImageSize imageSize) {
 		this.pixelSize = imageSize;
@@ -99,42 +167,17 @@
 		}
 	}
 
-	/**
-	 * Sets the mimetype.
-	 * @param mimetype The mimetype to set
-	 */
-	public void setMimetype(String filetype) {
-		this.mimetype = filetype;
-	}
-
-	/**
-	 * @return ImageFileset
-	 */
-	public ImageFileset getParent() {
-		return parent;
-	}
+    /* (non-Javadoc)
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        // try to use File.toString
+        if (file != null) {
+            return file.toString();
+        }
+        return super.toString();
+    }
 
-	/**
-	 * Sets the parent.
-	 * @param parent The parent to set
-	 */
-	public void setParent(ImageFileset parent) {
-		this.parent = parent;
-	}
-
-	/**
-	 * @return boolean
-	 */
-	public boolean isChecked() {
-		return (pixelSize != null);
-	}
 	
-	/** Returns the aspect ratio of the image (width/height).
-	 * 
-	 * @return
-	 */
-	public float getAspect() {
-		return (pixelSize != null) ? pixelSize.getAspect() : 0;
-	}
-
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/ImageFileSet.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,307 @@
+/**
+ * 
+ */
+package digilib.io;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+
+import digilib.io.FileOps.FileClass;
+import digilib.meta.MetadataMap;
+import digilib.meta.XMLMetaLoader;
+
+/**
+ * @author casties
+ *
+ */
+public class ImageFileSet extends ImageSet implements DocuDirent {
+
+    /** this is an image file */
+    protected static FileClass fileClass = FileClass.IMAGE;
+    /** the (main) file */
+    protected File file = null;
+    /** the file name */
+    protected String name = null;
+	/** HashMap with metadata */
+	protected MetadataMap fileMeta = null;
+	/** Is the Metadata valid */
+	protected boolean metaChecked = false;
+	/** the parent directory */
+	protected Directory parentDir = null;
+    
+    /**
+     * Constructor with a File and Directories.
+     * 
+     * @param file
+     * @param scaleDirs
+     */
+    public ImageFileSet(File file, Directory[] scaleDirs) {
+        int nb = scaleDirs.length;
+        list = new ArrayList<ImageInput>(nb);
+        // first dir is our parent
+        parentDir = scaleDirs[0];
+        this.file = file;
+        this.name = file.getName();
+        fill(scaleDirs, file);
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.DocuDirent#getName()
+     */
+    public String getName() {
+    	return this.name;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.DocuDirent#getParent()
+     */
+    public Directory getParent() {
+    	return this.parentDir;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.DocuDirent#setParent(digilib.io.Directory)
+     */
+    public void setParent(Directory parent) {
+    	this.parentDir = parent;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.DocuDirent#getFileMeta()
+     */
+    public MetadataMap getFileMeta() {
+    	return this.fileMeta;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.DocuDirent#setFileMeta(digilib.io.MetadataMap)
+     */
+    public void setFileMeta(MetadataMap fileMeta) {
+    	this.fileMeta = fileMeta;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.DocuDirent#isMetaChecked()
+     */
+    public boolean isMetaChecked() {
+    	return this.metaChecked;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.DocuDirent#compareTo(java.lang.Object)
+     */
+    public int compareTo(Object arg0) {
+		if (arg0 instanceof DocuDirent) {
+		    return name.compareTo(((DocuDirent) arg0).getName());
+		} else {
+		    return getName().compareTo((String) arg0);
+		}
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.io.DocuDirent#getFile()
+     */
+    public File getFile() {
+        return file;
+    }
+
+    /**
+     * Adds an ImageFile to this Fileset.
+     * 
+     * The files should be added in the order of higher to lower resolutions.
+     * The first file is considered the hires "original".
+     * 
+     * 
+     * @param f
+     *            file to add
+     * @return true (always)
+     */
+    public boolean add(ImageInput f) {
+    	f.setParent(this);
+    	return list.add(f);
+    }
+
+    /**
+     * Fill the ImageSet with files from different base directories.
+     * 
+     * 
+     * @param dirs
+     *            list of base directories
+     * @param fl
+     *            file (from first base dir)
+     * @param hints
+     *  
+     */
+    void fill(Directory[] dirs, File fl) {
+    	String fn = fl.getName();
+    	String baseFn = FileOps.basename(fn);
+    	// add the first ImageFile to the ImageSet
+    	add(new ImageFile(fl, this, parentDir));
+    	// iterate the remaining base directories
+    	for (int i = 1; i < dirs.length; ++i) {
+    	    Directory dir = dirs[i];
+    		if (dir == null) {
+    			continue;
+    		}
+    		// read the directory
+    		if (dir.getFilenames() == null) {
+    			dir.readDir();
+    		}
+    		String[] dirFiles = dir.getFilenames();
+    		// try the same filename as the original
+    		int fileIdx = Arrays.binarySearch(dirFiles, fn);
+    		if (fileIdx < 0) {
+    			// try closest matches without extension
+    			fileIdx = -fileIdx - 1;
+    			// try idx
+    			if ((fileIdx < dirFiles.length)
+    					&& (FileOps.basename(dirFiles[fileIdx]).equals(baseFn))) {
+    				// idx ok
+    			} else if ((fileIdx > 0)
+    					&& (FileOps.basename(dirFiles[fileIdx - 1])
+    							.equals(baseFn))) {
+    				// idx-1 ok
+    				fileIdx = fileIdx - 1;
+    			} else if ((fileIdx+1 < dirFiles.length)
+    					&& (FileOps.basename(dirFiles[fileIdx + 1])
+    							.equals(baseFn))) {
+    				// idx+1 ok
+    				fileIdx = fileIdx + 1;
+    			} else {
+    				// basename doesn't match
+    				continue;
+    			}
+    		}
+    		if (FileOps.classForFilename(dirFiles[fileIdx]) == fileClass) {
+    			/* logger.debug("adding file " + dirFiles[fileIdx]
+    					+ " to Fileset " + this.getName()); */
+    			add(new ImageFile(dirFiles[fileIdx], this, dir));
+    		}
+    	}
+    }
+
+    /**
+     * Checks metadata and sets resolution in resX and resY.
+     *  
+     */
+    public void checkMeta() {
+        if (metaChecked) {
+            return;
+        }
+        if (fileMeta == null) {
+            // try to read metadata file
+            readMeta();
+            if (fileMeta == null) {
+                // try directory metadata
+                ((DocuDirectory) parentDir).checkMeta();
+                if (((DocuDirectory) parentDir).getDirMeta() != null) {
+                    fileMeta = ((DocuDirectory) parentDir).getDirMeta();
+                } else {
+                    // try parent directory metadata
+                    DocuDirectory gp = (DocuDirectory) parentDir.getParent();
+                    if (gp != null) {
+                        gp.checkMeta();
+                        if (gp.getDirMeta() != null) {
+                            fileMeta = gp.getDirMeta();
+                        }
+                    }
+                }
+            }
+        }
+        if (fileMeta == null) {
+            // no metadata available
+            metaChecked = true;
+            return;
+        }
+        metaChecked = true;
+        float dpi = 0;
+        float dpix = 0;
+        float dpiy = 0;
+        float sizex = 0;
+        float sizey = 0;
+        float pixx = 0;
+        float pixy = 0;
+        // DPI is valid for X and Y
+        if (fileMeta.containsKey("original-dpi")) {
+            try {
+                dpi = Float.parseFloat((String) fileMeta.get("original-dpi"));
+            } catch (NumberFormatException e) {
+            }
+            if (dpi != 0) {
+                resX = dpi;
+                resY = dpi;
+                return;
+            }
+        }
+        // DPI-X and DPI-Y
+        if (fileMeta.containsKey("original-dpi-x")
+                && fileMeta.containsKey("original-dpi-y")) {
+            try {
+                dpix = Float.parseFloat((String) fileMeta
+                        .get("original-dpi-x"));
+                dpiy = Float.parseFloat((String) fileMeta
+                        .get("original-dpi-y"));
+            } catch (NumberFormatException e) {
+            }
+            if ((dpix != 0) && (dpiy != 0)) {
+                resX = dpix;
+                resY = dpiy;
+                return;
+            }
+        }
+        // SIZE-X and SIZE-Y and PIXEL-X and PIXEL-Y
+        if (fileMeta.containsKey("original-size-x")
+                && fileMeta.containsKey("original-size-y")
+                && fileMeta.containsKey("original-pixel-x")
+                && fileMeta.containsKey("original-pixel-y")) {
+            try {
+                sizex = Float.parseFloat((String) fileMeta
+                        .get("original-size-x"));
+                sizey = Float.parseFloat((String) fileMeta
+                        .get("original-size-y"));
+                pixx = Float.parseFloat((String) fileMeta
+                        .get("original-pixel-x"));
+                pixy = Float.parseFloat((String) fileMeta
+                        .get("original-pixel-y"));
+            } catch (NumberFormatException e) {
+            }
+            if ((sizex != 0) && (sizey != 0) && (pixx != 0) && (pixy != 0)) {
+                resX = pixx / (sizex * 100 / 2.54f);
+                resY = pixy / (sizey * 100 / 2.54f);
+                return;
+            }
+        }
+    }
+
+	/* (non-Javadoc)
+     * @see digilib.io.DocuDirent#readMeta()
+     */
+	public void readMeta() {
+		if ((fileMeta != null) || (file == null)) {
+			// there is already metadata or there is no file
+			return;
+		}
+		// metadata is in the file {filename}.meta
+		String fn = file.getAbsolutePath();
+		File mf = new File(fn + ".meta");
+		if (mf.canRead()) {
+			XMLMetaLoader ml = new XMLMetaLoader();
+			try {
+				// read meta file
+				Map<String, MetadataMap> meta = ml.loadURL(mf.getAbsolutePath());
+				if (meta == null) {
+					return;
+				}
+				fileMeta = meta.get(name);
+			} catch (Exception e) {
+				Logger.getLogger(this.getClass()).warn("error reading file .meta", e);
+			}
+		}
+	}
+
+
+}
--- a/servlet/src/digilib/io/ImageFileset.java	Tue Mar 01 17:12:25 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,414 +0,0 @@
-/* ImageFileset -- digilib image file info class.  
- * Digital Image Library servlet components  
- * Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)  
- * 
- * This program is free software; you can
- * redistribute it and/or modify it under the terms of the GNU General Public
- * License as published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.  
- * 
- * Please read license.txt for the full details. A copy of the GPL may be 
- * found at http://www.gnu.org/copyleft/lgpl.html  
- * 
- * You should have received a copy of the GNU General Public License along 
- * with this program; if not, write to the Free Software Foundation, Inc., 
- * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA  
- */
-
-package digilib.io;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-
-import digilib.image.ImageSize;
-import digilib.io.FileOps.FileClass;
-import digilib.servlet.DigilibConfiguration;
-
-/**
- * @author casties
- */
-public class ImageFileset extends DocuDirent {
-
-	/** this is an image file */
-	protected static FileClass fileClass = FileClass.IMAGE;
-	
-	/** list of files (ImageFile) */
-	private List<ImageFile> list = null;
-
-	/** aspect ratio (width/height) */
-	private float aspect = 0;
-
-	/** resolution of the biggest image (DPI) */
-	private float resX = 0;
-
-	/** resolution of the biggest image (DPI) */
-	private float resY = 0;
-
-	/**
-	 * Creator for empty fileset.
-	 * 
-	 * 
-	 * @param initialCapacity
-	 */
-	public ImageFileset() {
-		list = new ArrayList<ImageFile>();
-	}
-
-	/**
-	 * Constructor with a file and hints.
-	 * 
-	 * The hints are expected to contain 'basedirs' and 'scaledfilext' keys.
-	 * 
-	 * @param file
-	 * @param hints
-	 */
-	public ImageFileset(File file, Map<Integer,Object> hints) {
-		Directory[] dirs = (Directory[]) hints.get(FileOps.HINT_BASEDIRS);
-		int nb = dirs.length;
-		list = new ArrayList<ImageFile>(nb);
-		parent = dirs[0];
-		fill(dirs, file, hints);
-	}
-
-	/**
-	 * Adds an ImageFile to this Fileset.
-	 * 
-	 * The files should be added in the order of higher to lower resolutions.
-	 * The first file is considered the hires "original".
-	 * 
-	 * 
-	 * @param f
-	 *            file to add
-	 * @return true (always)
-	 */
-	public boolean add(ImageFile f) {
-		f.setParent(this);
-		return list.add(f);
-	}
-
-	/**
-	 * The number of image files in this Fileset.
-	 * 
-	 * 
-	 * @return number of image files
-	 */
-	public int size() {
-		return (list != null) ? list.size() : 0;
-	}
-
-	/**
-	 * Gets the default File.
-	 *  
-	 */
-	public File getFile() {
-		return (list != null) ? list.get(0).getFile() : null;
-	}
-
-	/**
-	 * Get the ImageFile at the index.
-	 * 
-	 * 
-	 * @param index
-	 * @return
-	 */
-	public ImageFile get(int index) {
-		return list.get(index);
-	}
-
-	/**
-	 * Get the next smaller ImageFile than the given size.
-	 * 
-	 * Returns the ImageFile from the set that has a width and height smaller or
-	 * equal the given size. Returns null if there isn't any smaller image.
-	 * Needs DocuInfo instance to checkFile().
-	 * 
-	 * 
-	 * @param size
-	 * @param info
-	 * @return
-	 */
-	public ImageFile getNextSmaller(ImageSize size) {
-		for (Iterator<ImageFile> i = getHiresIterator(); i.hasNext();) {
-			ImageFile f = i.next();
-			try {
-				if (!f.isChecked()) {
-					DigilibConfiguration.docuImageIdentify(f);
-				}
-				if (f.getSize().isTotallySmallerThan(size)) {
-					return f;
-				}
-			} catch (IOException e) {
-			}
-		}
-		return null;
-	}
-
-	/**
-	 * Get the next bigger ImageFile than the given size.
-	 * 
-	 * Returns the ImageFile from the set that has a width or height bigger or
-	 * equal the given size. Returns null if there isn't any bigger image. Needs
-	 * DocuInfo instance to checkFile().
-	 * 
-	 * 
-	 * @param size
-	 * @param info
-	 * @return
-	 */
-	public ImageFile getNextBigger(ImageSize size) {
-		for (ListIterator<ImageFile> i = getLoresIterator(); i.hasPrevious();) {
-			ImageFile f = i.previous();
-			try {
-				if (!f.isChecked()) {
-					DigilibConfiguration.docuImageIdentify(f);
-				}
-				if (f.getSize().isBiggerThan(size)) {
-					return f;
-				}
-			} catch (IOException e) {
-			}
-		}
-		return null;
-	}
-
-	/**
-	 * Returns the biggest ImageFile in the set.
-	 * 
-	 * 
-	 * @return
-	 */
-	public ImageFile getBiggest() {
-		return this.get(0);
-	}
-
-	/**
-	 * Returns the biggest ImageFile in the set.
-	 * 
-	 * 
-	 * @return
-	 */
-	public ImageFile getSmallest() {
-		return this.get(this.size() - 1);
-	}
-
-	/**
-	 * Get an Iterator for this Fileset starting at the highest resolution
-	 * images.
-	 * 
-	 * 
-	 * @return
-	 */
-	public ListIterator<ImageFile> getHiresIterator() {
-		return list.listIterator();
-	}
-
-	/**
-	 * Get an Iterator for this Fileset starting at the lowest resolution
-	 * images.
-	 * 
-	 * The Iterator starts at the last element, so you have to use it backwards
-	 * with hasPrevious() and previous().
-	 * 
-	 * 
-	 * @return
-	 */
-	public ListIterator<ImageFile> getLoresIterator() {
-		return list.listIterator(list.size());
-	}
-
-	/**
-	 * Fill the ImageFileset with files from different base directories.
-	 * 
-	 * 
-	 * @param dirs
-	 *            list of base directories
-	 * @param fl
-	 *            file (from first base dir)
-	 * @param hints
-	 *  
-	 */
-	void fill(Directory[] dirs, File fl, Map<Integer,Object> hints) {
-		int nb = dirs.length;
-		String fn = fl.getName();
-		String baseFn = FileOps.basename(fn);
-		// add the first ImageFile to the ImageFileset
-		add(new ImageFile(fn, this, parent));
-		// iterate the remaining base directories
-		for (int dirIdx = 1; dirIdx < nb; dirIdx++) {
-			if (dirs[dirIdx] == null) {
-				continue;
-			}
-			// read the directory
-			if (dirs[dirIdx].getFilenames() == null) {
-				dirs[dirIdx].readDir();
-			}
-			String[] dirFiles = dirs[dirIdx].getFilenames();
-			// try the same filename as the original
-			int fileIdx = Arrays.binarySearch(dirFiles, fn);
-			if (fileIdx < 0) {
-				// try closest matches without extension
-				fileIdx = -fileIdx - 1;
-				// try idx
-				if ((fileIdx < dirFiles.length)
-						&& (FileOps.basename(dirFiles[fileIdx]).equals(baseFn))) {
-					// idx ok
-				} else if ((fileIdx > 0)
-						&& (FileOps.basename(dirFiles[fileIdx - 1])
-								.equals(baseFn))) {
-					// idx-1 ok
-					fileIdx = fileIdx - 1;
-				} else if ((fileIdx+1 < dirFiles.length)
-						&& (FileOps.basename(dirFiles[fileIdx + 1])
-								.equals(baseFn))) {
-					// idx+1 ok
-					fileIdx = fileIdx + 1;
-				} else {
-					// basename doesn't match
-					continue;
-				}
-			}
-			if (FileOps.classForFilename(dirFiles[fileIdx]) == fileClass) {
-				/* logger.debug("adding file " + dirFiles[fileIdx]
-						+ " to Fileset " + this.getName()); */
-				add(new ImageFile(dirFiles[fileIdx], this, dirs[dirIdx]));
-			}
-		}
-	}
-
-	/**
-	 * Checks metadata and sets resolution in resX and resY.
-	 *  
-	 */
-	public void checkMeta() {
-		if (metaChecked) {
-			return;
-		}
-		if (fileMeta == null) {
-			// try to read metadata file
-			readMeta();
-			if (fileMeta == null) {
-				// try directory metadata
-				((DocuDirectory) parent).checkMeta();
-				if (((DocuDirectory) parent).getDirMeta() != null) {
-					fileMeta = ((DocuDirectory) parent).getDirMeta();
-				} else {
-					// try parent directory metadata
-					DocuDirectory gp = (DocuDirectory) parent.getParent();
-					if (gp != null) {
-						gp.checkMeta();
-						if (gp.getDirMeta() != null) {
-							fileMeta = gp.getDirMeta();
-						}
-					}
-				}
-			}
-		}
-		if (fileMeta == null) {
-			// no metadata available
-			metaChecked = true;
-			return;
-		}
-		metaChecked = true;
-		float dpi = 0;
-		float dpix = 0;
-		float dpiy = 0;
-		float sizex = 0;
-		float sizey = 0;
-		float pixx = 0;
-		float pixy = 0;
-		// DPI is valid for X and Y
-		if (fileMeta.containsKey("original-dpi")) {
-			try {
-				dpi = Float.parseFloat((String) fileMeta.get("original-dpi"));
-			} catch (NumberFormatException e) {
-			}
-			if (dpi != 0) {
-				resX = dpi;
-				resY = dpi;
-				return;
-			}
-		}
-		// DPI-X and DPI-Y
-		if (fileMeta.containsKey("original-dpi-x")
-				&& fileMeta.containsKey("original-dpi-y")) {
-			try {
-				dpix = Float.parseFloat((String) fileMeta
-						.get("original-dpi-x"));
-				dpiy = Float.parseFloat((String) fileMeta
-						.get("original-dpi-y"));
-			} catch (NumberFormatException e) {
-			}
-			if ((dpix != 0) && (dpiy != 0)) {
-				resX = dpix;
-				resY = dpiy;
-				return;
-			}
-		}
-		// SIZE-X and SIZE-Y and PIXEL-X and PIXEL-Y
-		if (fileMeta.containsKey("original-size-x")
-				&& fileMeta.containsKey("original-size-y")
-				&& fileMeta.containsKey("original-pixel-x")
-				&& fileMeta.containsKey("original-pixel-y")) {
-			try {
-				sizex = Float.parseFloat((String) fileMeta
-						.get("original-size-x"));
-				sizey = Float.parseFloat((String) fileMeta
-						.get("original-size-y"));
-				pixx = Float.parseFloat((String) fileMeta
-						.get("original-pixel-x"));
-				pixy = Float.parseFloat((String) fileMeta
-						.get("original-pixel-y"));
-			} catch (NumberFormatException e) {
-			}
-			if ((sizex != 0) && (sizey != 0) && (pixx != 0) && (pixy != 0)) {
-				resX = pixx / (sizex * 100 / 2.54f);
-				resY = pixy / (sizey * 100 / 2.54f);
-				return;
-			}
-		}
-	}
-
-	/**
-	 * @return
-	 */
-	public float getResX() {
-		return resX;
-	}
-
-	/**
-	 * @return
-	 */
-	public float getResY() {
-		return resY;
-	}
-
-	/**
-	 * Sets the aspect ratio from an ImageSize.
-	 * 
-	 * 
-	 * @param f
-	 */
-	public void setAspect(ImageSize s) {
-		aspect = s.getAspect();
-	}
-
-	/**
-	 * Returns the aspect ratio.
-	 * 
-	 * Aspect ratio is (width/height). So it's <1 for portrait and >1 for
-	 * landscape.
-	 * 
-	 * 
-	 * @return
-	 */
-	public float getAspect() {
-		return aspect;
-	}
-
-}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/ImageInput.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,159 @@
+/* ImageInput-- digilib image input interface.
+
+  Digital Image Library servlet components
+
+  Copyright (C) 2010 Robert Casties (robcast@mail.berlios.de)
+
+  This program is free software; you can redistribute  it and/or modify it
+  under  the terms of  the GNU General  Public License as published by the
+  Free Software Foundation;  either version 2 of the  License, or (at your
+  option) any later version.
+   
+  Please read license.txt for the full details. A copy of the GPL
+  may be found at http://www.gnu.org/copyleft/lgpl.html
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+ * Created on 20.12.2010
+ */
+
+package digilib.io;
+
+import java.io.File;
+import java.io.InputStream;
+
+import javax.imageio.stream.ImageInputStream;
+
+import digilib.util.ImageSize;
+
+public abstract class ImageInput {
+
+	// mime file type
+	protected String mimetype = null;
+	// image size in pixels
+	protected ImageSize pixelSize = null;
+    protected ImageSet parent = null;
+
+	/**
+	 * @return ImageSize
+	 */
+	public ImageSize getSize() {
+		return pixelSize;
+	}
+
+	/**
+	 * Sets the imageSize.
+	 * @param imageSize The imageSize to set
+	 */
+	public void setSize(ImageSize imageSize) {
+		this.pixelSize = imageSize;
+	}
+
+    /** returns if mimetype has been set.
+     * 
+     * @return String
+     */
+    public boolean hasMimetype() {
+        return (mimetype != null);
+    }
+
+    /**
+	 * @return String
+	 */
+	public String getMimetype() {
+		return mimetype;
+	}
+
+	/**
+	 * Sets the mimetype.
+	 * @param mimetype The mimetype to set
+	 */
+	public void setMimetype(String filetype) {
+		this.mimetype = filetype;
+	}
+
+	/** returns if this image has been checked 
+	 * (i.e. has size and mimetype)
+	 * TODO: deprecated
+	 * @return boolean
+	 */
+	public boolean isChecked() {
+		return (pixelSize != null);
+	}
+	
+	/** Returns the aspect ratio of the image (width/height).
+	 * 
+	 * @return
+	 */
+	public float getAspect() {
+		return (pixelSize != null) ? pixelSize.getAspect() : 0f;
+	}
+	
+    /**
+     * @return ImageSet
+     */
+    public ImageSet getParent() {
+        return parent;
+    }
+
+    /**
+     * Sets the parent.
+     * @param parent The parent to set
+     */
+    public void setParent(ImageSet parent) {
+        this.parent = parent;
+    }
+
+    /** Returns if the input can be returned as ImageInputStream.
+	 * 
+	 * @return
+	 */
+	public boolean hasImageInputStream() {
+		return false;
+	}
+	
+	/** Returns the input as ImageInputStream (if available)
+	 * 
+	 * @return
+	 */
+	public ImageInputStream getImageInputStream() {
+		return null;
+	}
+	
+    /** Returns if the input can be returned as InputStream.
+     * 
+     * @return
+     */
+    public boolean hasInputStream() {
+        return false;
+    }
+    
+    /** Returns the input as InputStream (if available)
+     * 
+     * @return
+     */
+    public InputStream getInputStream() {
+        return null;
+    }
+    
+	/** Returns if the input can be returned as File.
+	 * 
+	 * @return
+	 */
+	public boolean hasFile() {
+		return false;
+	}
+	
+	/** Returns the input as File (if available)
+	 * 
+	 * @return
+	 */
+	public File getFile() {
+		return null;
+	}
+
+	
+	
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/ImageSet.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,211 @@
+/* ImageSet -- digilib image file info class.  
+ * Digital Image Library servlet components  
+ * Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)  
+ * 
+ * This program is free software; you can
+ * redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.  
+ * 
+ * Please read license.txt for the full details. A copy of the GPL may be 
+ * found at http://www.gnu.org/copyleft/lgpl.html  
+ * 
+ * You should have received a copy of the GNU General Public License along 
+ * with this program; if not, write to the Free Software Foundation, Inc., 
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA  
+ */
+
+package digilib.io;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+
+import digilib.util.ImageSize;
+
+/**
+ * @author casties
+ */
+public class ImageSet {
+
+	/** list of files (ImageFile) */
+	protected List<ImageInput> list = null;
+
+	/** aspect ratio (width/height) */
+	protected float aspect = 0f;
+
+	/** resolution of the biggest image (DPI) */
+	protected float resX = 0f;
+
+	/** resolution of the biggest image (DPI) */
+	protected float resY = 0f;
+
+	/**
+	 * Creator for empty fileset.
+	 * 
+	 * 
+	 * @param initialCapacity
+	 */
+	public ImageSet() {
+		list = new ArrayList<ImageInput>();
+	}
+
+	/**
+	 * The number of image files in this Fileset.
+	 * 
+	 * 
+	 * @return number of image files
+	 */
+	public int size() {
+		return (list != null) ? list.size() : 0;
+	}
+
+	/**
+	 * Gets the default Input.
+	 *  
+	 */
+	public ImageInput get() {
+		return (list != null) ? list.get(0) : null;
+	}
+
+	/**
+	 * Get the ImageFile at the index.
+	 * 
+	 * 
+	 * @param index
+	 * @return
+	 */
+	public ImageInput get(int index) {
+		return list.get(index);
+	}
+
+	/**
+	 * Get the next smaller ImageFile than the given size.
+	 * 
+	 * Returns the ImageFile from the set that has a width and height smaller or
+	 * equal the given size. Returns null if there isn't any smaller image.
+	 * 
+	 * @param size
+	 * @param info
+	 * @return
+	 */
+	public ImageInput getNextSmaller(ImageSize size) {
+		for (ListIterator<ImageInput> i = getHiresIterator(); i.hasNext();) {
+			ImageInput f = i.next();
+            ImageSize is = f.getSize();
+            if (is != null && is.isTotallySmallerThan(size)) {
+				return f;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Get the next bigger ImageFile than the given size.
+	 * 
+	 * Returns the ImageFile from the set that has a width or height bigger or
+	 * equal the given size. Returns null if there isn't any bigger image.
+	 * 
+	 * @param size
+	 * @param info
+	 * @return
+	 */
+	public ImageInput getNextBigger(ImageSize size) {
+		for (ListIterator<ImageInput> i = getLoresIterator(); i.hasPrevious();) {
+			ImageInput f = i.previous();
+			ImageSize is = f.getSize();
+			if (is != null && is.isBiggerThan(size)) {
+				return f;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Returns the biggest ImageFile in the set.
+	 * 
+	 * 
+	 * @return
+	 */
+	public ImageInput getBiggest() {
+		return this.get(0);
+	}
+
+	/**
+	 * Returns the biggest ImageFile in the set.
+	 * 
+	 * 
+	 * @return
+	 */
+	public ImageInput getSmallest() {
+		return this.get(this.size() - 1);
+	}
+
+	/**
+	 * Get an Iterator for this Fileset starting at the highest resolution
+	 * images.
+	 * 
+	 * 
+	 * @return
+	 */
+	public ListIterator<ImageInput> getHiresIterator() {
+		return list.listIterator();
+	}
+
+	/**
+	 * Get an Iterator for this Fileset starting at the lowest resolution
+	 * images.
+	 * 
+	 * The Iterator starts at the last element, so you have to use it backwards
+	 * with hasPrevious() and previous().
+	 * 
+	 * 
+	 * @return
+	 */
+	public ListIterator<ImageInput> getLoresIterator() {
+		return list.listIterator(list.size());
+	}
+
+	/**
+	 * @return
+	 */
+	public float getResX() {
+		return resX;
+	}
+
+	/**
+	 * @return
+	 */
+	public float getResY() {
+		return resY;
+	}
+
+	/**
+	 * Sets the aspect ratio from an ImageSize.
+	 * 
+	 * 
+	 * @param f
+	 */
+	public void setAspect(ImageSize s) {
+		aspect = s.getAspect();
+	}
+
+	/**
+	 * Returns the aspect ratio.
+	 * 
+	 * Aspect ratio is (width/height). So it's <1 for portrait and >1 for
+	 * landscape.
+	 * 
+	 * 
+	 * @return
+	 */
+	public float getAspect() {
+		return aspect;
+	}
+
+    public void checkMeta() {
+        // TODO Auto-generated method stub
+        
+    }
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/ImageStream.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,41 @@
+/**
+ * 
+ */
+package digilib.io;
+
+import java.io.InputStream;
+
+/**
+ * @author casties
+ * 
+ */
+public class ImageStream extends ImageInput {
+
+    protected InputStream stream = null;
+
+    public ImageStream(InputStream stream, String mimeType) {
+        this.stream = stream;
+        this.mimetype = mimeType;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see digilib.io.ImageInput#hasInputStream()
+     */
+    @Override
+    public boolean hasInputStream() {
+        return true;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see digilib.io.ImageInput#getInputStream()
+     */
+    @Override
+    public InputStream getInputStream() {
+        return stream;
+    }
+
+}
--- a/servlet/src/digilib/io/MetadataMap.java	Tue Mar 01 17:12:25 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-/**
- * 
- */
-package digilib.io;
-
-import java.util.HashMap;
-
-/** Map for metadata related to files.
- * @author casties
- *
- */
-public class MetadataMap extends HashMap<String, String> {
-
-}
--- a/servlet/src/digilib/io/SVGFile.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/io/SVGFile.java	Tue Mar 01 22:00:50 2011 +0100
@@ -30,7 +30,7 @@
  * @author casties
  *
  */
-public class SVGFile extends DocuDirent {
+public class SVGFile extends DocuDirentImpl {
 	/** this is a text file */
 	protected static FileClass fileClass = FileClass.SVG;
 	/** our File instance */
--- a/servlet/src/digilib/io/TextFile.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/io/TextFile.java	Tue Mar 01 22:00:50 2011 +0100
@@ -30,7 +30,7 @@
  * @author casties
  *
  */
-public class TextFile extends DocuDirent {
+public class TextFile extends DocuDirentImpl {
 	/** this is a text file */
 	protected static FileClass fileClass = FileClass.TEXT;
 	/** our File instance */
--- a/servlet/src/digilib/io/XMLListLoader.java	Tue Mar 01 17:12:25 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,178 +0,0 @@
-/* XMLListLoader -- Load an XML list into a Hashtable
-
-  Digital Image Library servlet components
-
-  Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
-
-  This program is free software; you can redistribute  it and/or modify it
-  under  the terms of  the GNU General  Public License as published by the
-  Free Software Foundation;  either version 2 of the  License, or (at your
-  option) any later version.
-   
-  Please read license.txt for the full details. A copy of the GPL
-  may be found at http://www.gnu.org/copyleft/lgpl.html
-
-  You should have received a copy of the GNU General Public License
-  along with this program; if not, write to the Free Software
-  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-
-*/
-
-package digilib.io;
-
-// JAXP packages
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.Map;
-
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
-
-import org.apache.log4j.Logger;
-import org.xml.sax.Attributes;
-import org.xml.sax.SAXException;
-import org.xml.sax.SAXParseException;
-import org.xml.sax.helpers.DefaultHandler;
-
-/** Loads a simple XML list into a HashMap.
- * 
- * The XML file has an outer <code>list_tag</code>. Every entry is an 
- * <code>entry_tag</code> with two attributes: the <code>key_att</code>
- * key and the <code>value_att</code> value.
- * 
- * The file is read by the <code>loadURL</code> method, that returns a
- * HashMap with the key-value pairs.
- * 
- * @author casties
- */
-public class XMLListLoader {
-
-	private Logger logger = Logger.getLogger(this.getClass());
-	private String listTag = "list";
-	private String entryTag = "entry";
-	private String keyAtt = "key";
-	private String valueAtt = "value";
-
-	public XMLListLoader() {
-	}
-
-	public XMLListLoader(
-		String list_tag,
-		String entry_tag,
-		String key_att,
-		String value_att) {
-		logger.debug("xmlListLoader("+list_tag+","+entry_tag+","+key_att+","+value_att+")");
-		listTag = list_tag;
-		entryTag = entry_tag;
-		keyAtt = key_att;
-		valueAtt = value_att;
-	}
-
-	/**
-	 *  inner class XMLListParser to be called by the parser
-	 */
-	private class XMLListParser extends DefaultHandler {
-
-		private Map<String, String> listData;
-		private LinkedList<String> tagSpace;
-
-		public Map<String, String> getData() {
-			return listData;
-		}
-
-		// Parser calls this once at the beginning of a document
-		public void startDocument() throws SAXException {
-			listData = new HashMap<String, String>();
-			tagSpace = new LinkedList<String>();
-		}
-
-		// Parser calls this for each element in a document
-		public void startElement(
-			String namespaceURI,
-			String localName,
-			String qName,
-			Attributes atts)
-			throws SAXException {
-			//System.out.println("<"+qName);
-			// open a new namespace
-			tagSpace.addLast(qName);
-
-			// ist it an entry tag?
-			if (qName.equals(entryTag)) {
-				// is it inside a list tag?
-				if ((listTag.length() > 0) && (!tagSpace.contains(listTag))) {
-					logger.error("BOO: Entry "
-							+ entryTag
-							+ " not inside list "
-							+ listTag);
-					throw new SAXParseException(
-						"Entry " + entryTag + " not inside list " + listTag,
-						null);
-				}
-				// get the attributes
-				String key = atts.getValue(keyAtt);
-				String val = atts.getValue(valueAtt);
-				if ((key == null) || (val == null)) {
-					logger.error("BOO: Entry "
-							+ entryTag
-							+ " does not have Attributes "
-							+ keyAtt
-							+ ", "
-							+ valueAtt);
-					throw new SAXParseException(
-						"Entry "
-							+ entryTag
-							+ " does not have Attributes "
-							+ keyAtt
-							+ ", "
-							+ valueAtt,
-						null);
-				}
-				// add the values
-				//System.out.println("DATA: "+key+" = "+val);
-				listData.put(key, val);
-			}
-		}
-
-		public void endElement(
-			String namespaceURI,
-			String localName,
-			String qName)
-			throws SAXException {
-			// exit the namespace
-			tagSpace.removeLast();
-		}
-
-	}
-
-	/**
-	 *  load and parse a file (as URL)
-	 *    returns HashMap with list data
-	 */
-	public Map<String, String> loadURL(String path) throws SAXException, IOException {
-		//System.out.println("loadurl ("+path+")");
-		// Create a JAXP SAXParserFactory and configure it
-		SAXParserFactory spf = SAXParserFactory.newInstance();
-		spf.setNamespaceAware(true);
-
-		SAXParser parser = null;
-		try {
-			// Create a JAXP SAXParser
-			parser = spf.newSAXParser();
-
-		} catch (ParserConfigurationException e) {
-			throw new SAXException(e);
-		}
-
-		// create a list parser (keeps the data!)
-		XMLListParser listParser = new XMLListParser();
-
-		// Tell the SAXParser to parse the XML document
-		parser.parse(path, listParser);
-
-		return listParser.getData();
-	}
-
-}
--- a/servlet/src/digilib/io/XMLMetaLoader.java	Tue Mar 01 17:12:25 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,287 +0,0 @@
-/* XMLMetaLoader -- Load an XML format metadata into a Hashtable
-
-  Digital Image Library servlet components
-
-  Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)
-
-  This program is free software; you can redistribute  it and/or modify it
-  under  the terms of  the GNU General  Public License as published by the
-  Free Software Foundation;  either version 2 of the  License, or (at your
-  option) any later version.
-   
-  Please read license.txt for the full details. A copy of the GPL
-  may be found at http://www.gnu.org/copyleft/lgpl.html
-
-  You should have received a copy of the GNU General Public License
-  along with this program; if not, write to the Free Software
-  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-
-*/
-
-package digilib.io;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.Map;
-
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
-
-import org.apache.log4j.Logger;
-import org.xml.sax.Attributes;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
-
-public class XMLMetaLoader {
-
-	private Logger logger = Logger.getLogger(this.getClass());
-	private String metaTag = "meta";
-	private String fileTag = "file";
-	private String fileNameTag = "name";
-	private String filePathTag = "path";
-	private String imgTag = "img";
-	private String collectTag = "context";
-
-	public XMLMetaLoader() {
-	}
-
-	/**
-	 *  inner class XMLMetaParser to be called by the parser
-	 */
-	private class XMLMetaParser extends DefaultHandler {
-
-		private LinkedList<String> tags;
-		private Map<String, MetadataMap> files;
-		private MetadataMap meta;
-		private StringBuffer content;
-		private boolean collecting;
-		private StringBuffer collectedContent;
-		private String fileName;
-		private String filePath;
-
-		/**
-		 * extracts the elements name from either localName ln or qName qn.
-		 * 
-		 * @param ln localName
-		 * @param qn qName
-		 * @return element name
-		 */
-		private String getName(String ln, String qn) {
-			if (ln != null) {
-				if (ln.length() > 0) {
-					return ln;
-				}
-			}
-			// else it's qName (or nothing)
-			return qn;
-		}
-
-		/**
-		 * returns all attributes as a String
-		 * 
-		 * @param attrs
-		 * @return 
-		 */
-		private String getAttrString(Attributes attrs) {
-			StringBuffer s = new StringBuffer();
-			for (int i = 0; i < attrs.getLength(); i++) {
-				String key = getName(attrs.getLocalName(i), attrs.getQName(i));
-				s.append(" "+key+"=\""+attrs.getValue(i)+"\"");
-			}
-			return s.toString();
-		}
-
-			
-		// Parser calls this once at the beginning of a document
-		public void startDocument() throws SAXException {
-			tags = new LinkedList<String>();
-			files = new HashMap<String, MetadataMap>();
-			collecting = false;
-			collectedContent = null;
-		}
-
-		// Parser calls this for each element in a document
-		public void startElement(
-			String namespaceURI,
-			String localName,
-			String qName,
-			Attributes atts)
-			throws SAXException {
-
-			String name = getName(localName, qName);
-			// open a new tag
-			tags.addLast(name);
-			// start new content (no nesting of tags and content)
-			content = new StringBuffer();
-
-			if (name.equals(metaTag)) {
-				// new meta tag
-				meta = new MetadataMap();
-				collectedContent = new StringBuffer();
-			} else if (name.equals(fileTag)) {
-				// new file tag
-				fileName = null;
-				filePath = null;
-				meta = new MetadataMap();
-				collectedContent = new StringBuffer();
-			} else if (name.equals(collectTag)) {
-				// start collecting
-				collecting = true;
-				if (collectedContent == null) {
-					collectedContent = new StringBuffer();
-				}
-			}
-			
-			// record mode
-			if (collecting) {
-				collectedContent.append("<"+name);
-				collectedContent.append(getAttrString(atts));
-				collectedContent.append(">");
-			}
-		}
-
-		// parser calls this for all tag content (possibly more than once)
-		public void characters(char[] ch, int start, int length)
-			throws SAXException {
-			// append data to current string buffer
-			if (content == null) {
-				content = new StringBuffer();
-			}
-			content.append(ch, start, length);
-		}
-
-		// parser calls this at the end of each element
-		public void endElement(
-			String namespaceURI,
-			String localName,
-			String qName)
-			throws SAXException {
-
-			String name = getName(localName, qName);
-			// exit the tag
-			tags.removeLast();
-			String lastTag = (tags.isEmpty()) ? "" : tags.getLast();
-
-			// was it a file/name tag?
-			if (name.equals(fileNameTag) && lastTag.equals(fileTag)) {
-				// save name as filename
-				if ((content != null) && (content.length() > 0)) {
-					fileName = content.toString().trim();
-				}
-				content = null;
-				return;
-			}
-
-			// was it a file/path tag?
-			if (name.equals(filePathTag) && lastTag.equals(fileTag)) {
-				// save path as filepath 
-				if ((content != null) && (content.length() > 0)) {
-					filePath = content.toString().trim();
-				}
-				content = null;
-				return;
-			}
-
-			// was it a file tag?
-			if (name.equals(fileTag)) {
-				// is there meta to save?
-				if ((meta != null) && (meta.size() > 0)) {
-					// file name is (optional file/path) / file/name
-					String fn = null;
-
-					if (fileName != null) {
-						if (filePath != null) {
-							fn = filePath + "/" + fileName;
-						} else {
-							fn = fileName;
-						}
-					} else {
-						// no file name, no file
-						content = null;
-						return;
-					}
-					// save meta in file list 
-					files.put(fn, meta);
-				}
-				content = null;
-				return;
-			}
-
-			// was it a meta tag outside a file tag?
-			if (name.equals(metaTag) && !tags.contains(fileTag)) {
-				// save meta as dir meta
-				if ((meta != null) && (meta.size() > 0)) {
-					files.put("", meta);
-				}
-				content = null;
-				return;
-			}
-
-			// is this inside an digilib info (=img) tag?
-			if (lastTag.equals(imgTag)) {
-				// then add whatever this is
-				if ((content != null) && (content.length() > 0)) {
-					meta.put(name, content.toString().trim());
-				}
-				content = null;
-				return;
-			}
-
-			// is this the end of collectTag?
-			if (name.equals(collectTag)) {
-				collecting = false;
-				collectedContent.append("</"+collectTag+">\n");
-				// store collected stuff
-				meta.put(collectTag, collectedContent.toString());
-				//logger.debug("collected: '"+collectedContent+"'");
-				content = null;
-				return;
-			}
-
-			// write collected content
-			if (collecting) {
-				String s = "";
-				if ((content != null) && (content.length() > 0)) {
-					s = content.toString().trim();
-				}
-				//logger.debug("collect:"+name+" = "+s);
-				collectedContent.append(s);
-				collectedContent.append("</"+name+">\n");
-				content = null;
-				return;
-			}
-		}
-
-	}
-
-	/**
-	 *  load and parse a file (as URL)
-	 *    returns HashMap with list data
-	 */
-	public Map<String, MetadataMap> loadURL(String path) throws SAXException, IOException {
-		logger.debug("loading meta: "+path);
-		// Create a JAXP SAXParserFactory and configure it
-		SAXParserFactory spf = SAXParserFactory.newInstance();
-		spf.setNamespaceAware(true);
-
-		SAXParser parser = null;
-		try {
-			// Create a JAXP SAXParser
-			parser = spf.newSAXParser();
-
-		} catch (ParserConfigurationException e) {
-			throw new SAXException(e);
-		}
-
-		// create a list parser (keeps the data!)
-		XMLMetaParser listParser = new XMLMetaParser();
-
-		// Tell the SAXParser to parse the XML document
-		parser.parse(path, listParser);
-
-		return listParser.files;
-	}
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/meta/MetadataMap.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,15 @@
+/**
+ * 
+ */
+package digilib.meta;
+
+import java.util.HashMap;
+
+/** Map for metadata related to files.
+ * @author casties
+ *
+ */
+@SuppressWarnings("serial")
+public class MetadataMap extends HashMap<String, String> {
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/meta/XMLMetaLoader.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,287 @@
+/* XMLMetaLoader -- Load an XML format metadata into a Hashtable
+
+  Digital Image Library servlet components
+
+  Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de)
+
+  This program is free software; you can redistribute  it and/or modify it
+  under  the terms of  the GNU General  Public License as published by the
+  Free Software Foundation;  either version 2 of the  License, or (at your
+  option) any later version.
+   
+  Please read license.txt for the full details. A copy of the GPL
+  may be found at http://www.gnu.org/copyleft/lgpl.html
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+*/
+
+package digilib.meta;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.apache.log4j.Logger;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class XMLMetaLoader {
+
+	private Logger logger = Logger.getLogger(this.getClass());
+	private String metaTag = "meta";
+	private String fileTag = "file";
+	private String fileNameTag = "name";
+	private String filePathTag = "path";
+	private String imgTag = "img";
+	private String collectTag = "context";
+
+	public XMLMetaLoader() {
+	}
+
+	/**
+	 *  inner class XMLMetaParser to be called by the parser
+	 */
+	private class XMLMetaParser extends DefaultHandler {
+
+		private LinkedList<String> tags;
+		private Map<String, MetadataMap> files;
+		private MetadataMap meta;
+		private StringBuffer content;
+		private boolean collecting;
+		private StringBuffer collectedContent;
+		private String fileName;
+		private String filePath;
+
+		/**
+		 * extracts the elements name from either localName ln or qName qn.
+		 * 
+		 * @param ln localName
+		 * @param qn qName
+		 * @return element name
+		 */
+		private String getName(String ln, String qn) {
+			if (ln != null) {
+				if (ln.length() > 0) {
+					return ln;
+				}
+			}
+			// else it's qName (or nothing)
+			return qn;
+		}
+
+		/**
+		 * returns all attributes as a String
+		 * 
+		 * @param attrs
+		 * @return 
+		 */
+		private String getAttrString(Attributes attrs) {
+			StringBuffer s = new StringBuffer();
+			for (int i = 0; i < attrs.getLength(); i++) {
+				String key = getName(attrs.getLocalName(i), attrs.getQName(i));
+				s.append(" "+key+"=\""+attrs.getValue(i)+"\"");
+			}
+			return s.toString();
+		}
+
+			
+		// Parser calls this once at the beginning of a document
+		public void startDocument() throws SAXException {
+			tags = new LinkedList<String>();
+			files = new HashMap<String, MetadataMap>();
+			collecting = false;
+			collectedContent = null;
+		}
+
+		// Parser calls this for each element in a document
+		public void startElement(
+			String namespaceURI,
+			String localName,
+			String qName,
+			Attributes atts)
+			throws SAXException {
+
+			String name = getName(localName, qName);
+			// open a new tag
+			tags.addLast(name);
+			// start new content (no nesting of tags and content)
+			content = new StringBuffer();
+
+			if (name.equals(metaTag)) {
+				// new meta tag
+				meta = new MetadataMap();
+				collectedContent = new StringBuffer();
+			} else if (name.equals(fileTag)) {
+				// new file tag
+				fileName = null;
+				filePath = null;
+				meta = new MetadataMap();
+				collectedContent = new StringBuffer();
+			} else if (name.equals(collectTag)) {
+				// start collecting
+				collecting = true;
+				if (collectedContent == null) {
+					collectedContent = new StringBuffer();
+				}
+			}
+			
+			// record mode
+			if (collecting) {
+				collectedContent.append("<"+name);
+				collectedContent.append(getAttrString(atts));
+				collectedContent.append(">");
+			}
+		}
+
+		// parser calls this for all tag content (possibly more than once)
+		public void characters(char[] ch, int start, int length)
+			throws SAXException {
+			// append data to current string buffer
+			if (content == null) {
+				content = new StringBuffer();
+			}
+			content.append(ch, start, length);
+		}
+
+		// parser calls this at the end of each element
+		public void endElement(
+			String namespaceURI,
+			String localName,
+			String qName)
+			throws SAXException {
+
+			String name = getName(localName, qName);
+			// exit the tag
+			tags.removeLast();
+			String lastTag = (tags.isEmpty()) ? "" : tags.getLast();
+
+			// was it a file/name tag?
+			if (name.equals(fileNameTag) && lastTag.equals(fileTag)) {
+				// save name as filename
+				if ((content != null) && (content.length() > 0)) {
+					fileName = content.toString().trim();
+				}
+				content = null;
+				return;
+			}
+
+			// was it a file/path tag?
+			if (name.equals(filePathTag) && lastTag.equals(fileTag)) {
+				// save path as filepath 
+				if ((content != null) && (content.length() > 0)) {
+					filePath = content.toString().trim();
+				}
+				content = null;
+				return;
+			}
+
+			// was it a file tag?
+			if (name.equals(fileTag)) {
+				// is there meta to save?
+				if ((meta != null) && (meta.size() > 0)) {
+					// file name is (optional file/path) / file/name
+					String fn = null;
+
+					if (fileName != null) {
+						if (filePath != null) {
+							fn = filePath + "/" + fileName;
+						} else {
+							fn = fileName;
+						}
+					} else {
+						// no file name, no file
+						content = null;
+						return;
+					}
+					// save meta in file list 
+					files.put(fn, meta);
+				}
+				content = null;
+				return;
+			}
+
+			// was it a meta tag outside a file tag?
+			if (name.equals(metaTag) && !tags.contains(fileTag)) {
+				// save meta as dir meta
+				if ((meta != null) && (meta.size() > 0)) {
+					files.put("", meta);
+				}
+				content = null;
+				return;
+			}
+
+			// is this inside an digilib info (=img) tag?
+			if (lastTag.equals(imgTag)) {
+				// then add whatever this is
+				if ((content != null) && (content.length() > 0)) {
+					meta.put(name, content.toString().trim());
+				}
+				content = null;
+				return;
+			}
+
+			// is this the end of collectTag?
+			if (name.equals(collectTag)) {
+				collecting = false;
+				collectedContent.append("</"+collectTag+">\n");
+				// store collected stuff
+				meta.put(collectTag, collectedContent.toString());
+				//logger.debug("collected: '"+collectedContent+"'");
+				content = null;
+				return;
+			}
+
+			// write collected content
+			if (collecting) {
+				String s = "";
+				if ((content != null) && (content.length() > 0)) {
+					s = content.toString().trim();
+				}
+				//logger.debug("collect:"+name+" = "+s);
+				collectedContent.append(s);
+				collectedContent.append("</"+name+">\n");
+				content = null;
+				return;
+			}
+		}
+
+	}
+
+	/**
+	 *  load and parse a file (as URL)
+	 *    returns HashMap with list data
+	 */
+	public Map<String, MetadataMap> loadURL(String path) throws SAXException, IOException {
+		logger.debug("loading meta: "+path);
+		// Create a JAXP SAXParserFactory and configure it
+		SAXParserFactory spf = SAXParserFactory.newInstance();
+		spf.setNamespaceAware(true);
+
+		SAXParser parser = null;
+		try {
+			// Create a JAXP SAXParser
+			parser = spf.newSAXParser();
+
+		} catch (ParserConfigurationException e) {
+			throw new SAXException(e);
+		}
+
+		// create a list parser (keeps the data!)
+		XMLMetaParser listParser = new XMLMetaParser();
+
+		// Tell the SAXParser to parse the XML document
+		parser.parse(path, listParser);
+
+		return listParser.files;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/pdf/DigilibInfoReader.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,76 @@
+package digilib.pdf;
+
+/** DigilibInfoReader 
+ * A class for reading the information from info.xml files used in digilib image directories.
+ *
+ */
+
+import java.io.File;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.input.SAXBuilder;
+
+
+
+public class DigilibInfoReader {
+
+	/** gengeral logger for this class */
+	protected static Logger logger = Logger.getLogger("digilib.servlet");
+	
+	private String filename = null;
+	//private static String base_element = "info";
+	
+	public DigilibInfoReader(String fn){
+		filename = fn;
+	}
+
+	/**
+	 * Returns the attribute defined by 'attr' as a String.
+	 * 
+	 * @param attr
+	 * @return
+	 */
+	@SuppressWarnings("unchecked") // Element.getChildren() returns naked List
+    public String getAsString(String attr){
+		try{
+			SAXBuilder builder = new SAXBuilder();
+			Document doc = builder.build(new File(filename));
+			Element root = doc.getRootElement();
+			List<Element> mainElements = root.getChildren();
+			// logger.debug("XML mainElements:"+mainElements.toString());
+
+			for(int i=0; i<mainElements.size(); i++){
+				Element elem = mainElements.get(i);
+				if(elem.getName()==attr){
+					// logger.debug(attr+" == "+(String)elem.getTextTrim());
+					return (String)elem.getTextTrim();
+				}
+			}
+
+		}
+		catch(Exception e){
+			logger.error(e.getMessage());
+		}
+		return null;
+	}
+	
+	
+	/**
+	 * Find out if the info.xml exists
+	 * @return
+	 */
+	public boolean hasInfo(){
+		try {
+			SAXBuilder builder = new SAXBuilder();
+			builder.build(new File(filename));
+			return true;
+		}
+		catch(Exception e){
+			return false;
+		}
+	}
+	
+}
--- a/servlet/src/digilib/pdf/PDFFileWorker.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/pdf/PDFFileWorker.java	Tue Mar 01 22:00:50 2011 +0100
@@ -46,10 +46,18 @@
     }
     
     public File call() throws Exception {
-    	OutputStream outstream = streamWorker.call();
-    	outstream.flush();
-    	// move temporary to final file
-    	tempFile.renameTo(finalFile);
+        OutputStream outstream = null;
+        try {
+            outstream = streamWorker.call();
+            outstream.flush();
+            outstream.close();
+            // move temporary to final file
+            tempFile.renameTo(finalFile);
+        } finally {
+            if (outstream != null) {
+                outstream.close();
+            }
+        }
         return finalFile;
     }
     
--- a/servlet/src/digilib/pdf/PDFStreamWorker.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/pdf/PDFStreamWorker.java	Tue Mar 01 22:00:50 2011 +0100
@@ -97,6 +97,7 @@
 		doc.close();
 		logger.debug("PDF: " + outstream + " doc.close() ("
 				+ (System.currentTimeMillis() - start_time) + "ms)");
+		docwriter.flush();
 		docwriter.close();
 		return outstream;
 	}
--- a/servlet/src/digilib/pdf/PDFTitlePage.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/pdf/PDFTitlePage.java	Tue Mar 01 22:00:50 2011 +0100
@@ -15,7 +15,6 @@
 import com.itextpdf.text.Image;
 import com.itextpdf.text.Paragraph;
 
-import digilib.io.DigilibInfoReader;
 import digilib.io.FileOpException;
 import digilib.servlet.PDFCache;
 import digilib.servlet.PDFRequest;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/servlet/AsyncServletWorker.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,94 @@
+/**
+ * 
+ */
+package digilib.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
+
+import digilib.image.DocuImage;
+import digilib.image.ImageJobDescription;
+import digilib.image.ImageOpException;
+import digilib.image.ImageWorker;
+import digilib.servlet.Scaler.ErrMsg;
+import digilib.servlet.Scaler.Error;
+
+/**
+ * @author casties
+ * 
+ */
+public class AsyncServletWorker implements Runnable {
+
+    /** the AsyncServlet context */
+    private AsyncContext asyncContext;
+
+    /** the ImageWorker we use */
+    private ImageWorker imageWorker;
+
+    protected static Logger logger = Logger.getLogger(AsyncServletWorker.class);
+    private long startTime;
+    private ErrMsg errMsgType = ErrMsg.IMAGE;
+	private ImageJobDescription jobinfo;
+
+    /**
+     * @param dlConfig
+     * @param jobinfo
+     */
+    public AsyncServletWorker(DigilibConfiguration dlConfig,
+            ImageJobDescription jobinfo, AsyncContext asyncContext,
+            ErrMsg errMsgType, long startTime) {
+        // set up image worker
+        imageWorker = new ImageWorker(dlConfig, jobinfo);
+        // save AsyncContext
+        this.asyncContext = asyncContext;
+        this.startTime = startTime;
+        this.errMsgType = errMsgType;
+        this.jobinfo = jobinfo;
+    }
+
+    /**
+     * runs the ImageWorker and writes the image to the ServletResponse.
+     */
+    public void run() {
+        // get fresh response
+        HttpServletResponse response = (HttpServletResponse) asyncContext.getResponse();
+        logger.debug("working on response: (" + ServletOps.headersToString(response) + ")");
+        try {
+            // render the image
+            DocuImage img = imageWorker.call();
+            // forced destination image type
+            String mt = null;
+            if (jobinfo.hasOption("jpg")) {
+            	mt = "image/jpeg";
+            } else if (jobinfo.hasOption("png")) {
+            	mt = "image/png";
+            }
+            // send image
+            ServletOps.sendImage(img, mt, response, logger);
+            logger.debug("Job done in: "
+                    + (System.currentTimeMillis() - startTime) + "ms");
+        } catch (ImageOpException e) {
+            logger.error(e.getClass() + ": " + e.getMessage());
+            Scaler.digilibError(errMsgType, Error.IMAGE, null, response);
+        } catch (IOException e) {
+            logger.error(e.getClass() + ": " + e.getMessage());
+            Scaler.digilibError(errMsgType, Error.FILE, null, response);
+        } catch (ServletException e) {
+            logger.error("Servlet error: ", e);
+        } catch (Exception e) {
+            logger.error("Other error: ", e);
+        } finally {
+            // submit response
+            logger.debug("context complete.");
+            logger.debug("response: (" + ServletOps.headersToString(response) + ")");
+            asyncContext.complete();
+        }
+
+    }
+
+}
--- a/servlet/src/digilib/servlet/DigilibConfiguration.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/servlet/DigilibConfiguration.java	Tue Mar 01 22:00:50 2011 +0100
@@ -26,7 +26,7 @@
 import java.util.Map;
 import java.util.Map.Entry;
 
-import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 
 import org.apache.log4j.BasicConfigurator;
@@ -35,10 +35,10 @@
 import digilib.image.DocuImage;
 import digilib.image.DocuImageImpl;
 import digilib.io.FileOps;
-import digilib.io.ImageFile;
-import digilib.io.XMLListLoader;
+import digilib.io.ImageInput;
 import digilib.util.Parameter;
 import digilib.util.ParameterMap;
+import digilib.util.XMLListLoader;
 
 /**
  * Class to hold the digilib servlet configuration parameters. The parameters
@@ -172,8 +172,8 @@
         newParameter("pdf-temp-dir", "pdf_temp", null, 'f');
         // PDF generation cache directory
         newParameter("pdf-cache-dir", "pdf_cache", null, 'f');
-        // PDF generation cache directory
-        newParameter("pdf-cache-dir", "pdf_cache", null, 'f');
+		// allow image toolkit to use disk cache
+		newParameter("img-diskcache-allowed", Boolean.TRUE, null, 'f');
 	}
 
 	/**
@@ -182,7 +182,7 @@
 	 * 
 	 * @see readConfig()
 	 */
-	public DigilibConfiguration(ServletConfig c) throws Exception {
+	public DigilibConfiguration(ServletContext c) throws Exception {
 		this();
 		readConfig(c);
 	}
@@ -192,7 +192,7 @@
 	 * or file digilib-config.xml
 	 */
 	@SuppressWarnings("unchecked")
-    public void readConfig(ServletConfig c) throws Exception {
+    public void readConfig(ServletContext c) throws Exception {
 
 		/*
 		 * Get config file name. The file name is first looked for as an init
@@ -215,7 +215,7 @@
 		XMLListLoader lilo =
 			new XMLListLoader("digilib-config", "parameter", "name", "value");
 		// read config file into HashMap
-		Map<String,String> confTable = lilo.loadURL(f.toURL().toString());
+		Map<String,String> confTable = lilo.loadURL(f.toString());
 
 		// set config file path parameter
 		setValue("servlet.config.file", f.getCanonicalPath());
@@ -277,7 +277,7 @@
 	 * @return
 	 * @throws IOException
 	 */
-	public static ImageFile docuImageIdentify(ImageFile imgf) throws IOException {
+	public static ImageInput identifyDocuImage(ImageInput imgf) throws IOException {
 	    // use fresh DocuImage instance
 	    DocuImage di = getDocuImageInstance();
 		return di.identify(imgf);
--- a/servlet/src/digilib/servlet/DigilibRequest.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/servlet/DigilibRequest.java	Tue Mar 01 22:00:50 2011 +0100
@@ -131,6 +131,8 @@
 		newParameter("ddpiy", new Float(0), null, 's');
 		// scale factor for mo=ascale
 		newParameter("scale", new Float(1), null, 's');
+		// color conversion operation
+		newParameter("colop", "", null, 's');
 
 		/*
 		 * Parameters of type 'i' are not exchanged between client and server,
--- a/servlet/src/digilib/servlet/DocumentBean.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/servlet/DocumentBean.java	Tue Mar 01 22:00:50 2011 +0100
@@ -33,12 +33,12 @@
 
 import digilib.auth.AuthOpException;
 import digilib.auth.AuthOps;
-import digilib.image.ImageSize;
 import digilib.io.DocuDirCache;
 import digilib.io.DocuDirectory;
 import digilib.io.FileOps.FileClass;
-import digilib.io.ImageFile;
-import digilib.io.ImageFileset;
+import digilib.io.ImageInput;
+import digilib.io.ImageSet;
+import digilib.util.ImageSize;
 
 public class DocumentBean {
 
@@ -191,13 +191,13 @@
 		}
 		String fn = dlRequest.getFilePath();
 		// get information about the file
-		ImageFileset fileset = (ImageFileset) dirCache.getFile(fn, dlRequest
+		ImageSet fileset = (ImageSet) dirCache.getFile(fn, dlRequest
 				.getAsInt("pn"), FileClass.IMAGE);
 		if (fileset == null) {
 			return;
 		}
 		// add file name
-		dlRequest.setValue("img.fn", fileset.getName());
+		dlRequest.setValue("img.fn", fileset);
 		// add dpi
 		dlRequest.setValue("img.dpix", new Double(fileset.getResX()));
 		dlRequest.setValue("img.dpiy", new Double(fileset.getResY()));
@@ -208,12 +208,8 @@
 			dlRequest.setValue("pt", dd.size());
 		}
 		// get original pixel size
-		ImageFile origfile = fileset.getBiggest();
-		// check image for size if mo=hires
-		if ((! origfile.isChecked())&&dlRequest.hasOption("hires")) {
-			logger.debug("pre-checking image!");
-			DigilibConfiguration.docuImageIdentify(origfile);
-		}
+		ImageInput origfile = fileset.getBiggest();
+		// check image for size (TODO: just if mo=hires?)
 		ImageSize pixsize = origfile.getSize();
 		if (pixsize != null) {
 			// add pixel size
--- a/servlet/src/digilib/servlet/Initialiser.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/servlet/Initialiser.java	Tue Mar 01 22:00:50 2011 +0100
@@ -24,10 +24,11 @@
 import java.io.OutputStream;
 import java.util.List;
 
-import javax.servlet.ServletConfig;
+import javax.imageio.ImageIO;
 import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.annotation.WebListener;
 
 import org.apache.log4j.Logger;
 import org.apache.log4j.xml.DOMConfigurator;
@@ -41,16 +42,17 @@
 import digilib.util.DigilibJobCenter;
 
 /**
- * Singleton initalisation servlet for setup tasks and resources.
+ * Singleton initalisation listener for setup tasks and resources.
  * 
  * @author casties
  *  
  */
-@SuppressWarnings("serial")
-public class Initialiser extends HttpServlet {
+@WebListener
+public class Initialiser implements ServletContextListener {
+
 
 	/** servlet version */
-	public static final String version = "0.2";
+	public static final String version = "0.3";
 
 	/** gengeral logger for this class */
 	private static Logger logger = Logger.getLogger("digilib.init");
@@ -61,7 +63,7 @@
 	/** DigilibConfiguration instance */
 	DigilibConfiguration dlConfig;
 
-	/** Executor for digilib image jobs */
+	/** Executor for digilib image jobs (AsyncServletWorker doesn't return anything) */
 	DigilibJobCenter<DocuImage> imageEx;
 	
 	/** Executor for PDF jobs */
@@ -72,24 +74,19 @@
 	
 	/**
 	 * Initialisation on first run.
-	 * 
-	 * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
 	 */
-	public void init(ServletConfig config) throws ServletException {
-		super.init(config);
+    public void contextInitialized(ServletContextEvent cte) {
+        ServletContext context = cte.getServletContext();
 
-		System.out
-				.println("***** Digital Image Library Initialisation Servlet (version "
+		System.out.println("***** Digital Image Library Initialiser (version "
 						+ version + ") *****");
 
-		// get our ServletContext
-		ServletContext context = config.getServletContext();
 		// see if there is a Configuration instance
 		dlConfig = (DigilibConfiguration) context.getAttribute("digilib.servlet.configuration");
 		if (dlConfig == null) {
 			// create new Configuration
 			try {
-				dlConfig = new DigilibConfiguration(config);
+				dlConfig = new DigilibConfiguration(context);
 
 				/*
 				 * further initialization
@@ -97,12 +94,12 @@
 
 				// set up the logger
 				File logConf = ServletOps.getConfigFile((File) dlConfig
-						.getValue("log-config-file"), config);
+						.getValue("log-config-file"), context);
 				DOMConfigurator.configure(logConf.getAbsolutePath());
 				dlConfig.setValue("log-config-file", logConf);
 				// say hello in the log file
 				logger
-						.info("***** Digital Image Library Initialisation Servlet (version "
+						.info("***** Digital Image Library Initialiser (version "
 								+ version + ") *****");
 				// directory cache
 				String[] bd = (String[]) dlConfig.getValue("basedir-list");
@@ -110,7 +107,7 @@
 				if (dlConfig.getAsBoolean("use-mapping")) {
 					// with mapping file
 					File mapConf = ServletOps.getConfigFile((File) dlConfig
-							.getValue("mapping-file"), config);
+							.getValue("mapping-file"), context);
 					dirCache = new AliasingDocuDirCache(bd, fcs, mapConf,
 							dlConfig);
 					dlConfig.setValue("mapping-file", mapConf);
@@ -125,7 +122,7 @@
 					//authOp = new DBAuthOpsImpl(util);
 					// XML version
 					File authConf = ServletOps.getConfigFile((File) dlConfig
-							.getValue("auth-file"), config);
+							.getValue("auth-file"), context);
 					AuthOps authOp = new XMLAuthOps(authConf);
 					dlConfig.setValue("servlet.auth.op", authOp);
 					dlConfig.setValue("auth-file", authConf);
@@ -133,6 +130,10 @@
 				// DocuImage class
 				DocuImage di = DigilibConfiguration.getDocuImageInstance();
 				dlConfig.setValue("servlet.docuimage.class", di.getClass().getName());
+				// disk cache for image toolkit
+				boolean dc = dlConfig.getAsBoolean("img-diskcache-allowed");
+				// TODO: methods for all toolkits?
+				ImageIO.setUseCache(dc);
 				// digilib worker threads
 				int nt = dlConfig.getAsInt("worker-threads");
                 int mt = dlConfig.getAsInt("max-waiting-threads");
@@ -152,22 +153,21 @@
 				context.setAttribute("digilib.servlet.configuration", dlConfig);
 
 			} catch (Exception e) {
-				throw new ServletException(e);
+				logger.error("Error in initialisation: ", e);
 			}
 		} else {
 			// say hello in the log file
-			logger
-					.info("***** Digital Image Library Initialisation Servlet (version "
+			logger.info("***** Digital Image Library Initialiser (version "
 							+ version + ") *****");
 			logger.warn("Already initialised!");
 		}
 	}
 
     /** clean up local resources
-     * @see javax.servlet.GenericServlet#destroy()
+     * 
      */
-    @Override
-    public void destroy() {
+    public void contextDestroyed(ServletContextEvent arg0) {
+        logger.info("Initialiser shutting down.");
         if (dirCache != null) {
             // shut down dirCache?
             dirCache = null;
@@ -196,7 +196,6 @@
                 logger.error("Still running threads when shutting down PDF-image job queue: "+nrj);
             }
         }
-        super.destroy();
     }
 
 }
--- a/servlet/src/digilib/servlet/Scaler.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/servlet/Scaler.java	Tue Mar 01 22:00:50 2011 +0100
@@ -3,12 +3,12 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
 
+import javax.servlet.AsyncContext;
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -20,17 +20,18 @@
 import digilib.image.DocuImage;
 import digilib.image.ImageJobDescription;
 import digilib.image.ImageOpException;
-import digilib.image.ImageWorker;
 import digilib.io.DocuDirCache;
 import digilib.io.DocuDirectory;
-import digilib.io.ImageFile;
+import digilib.io.ImageInput;
 import digilib.util.DigilibJobCenter;
 
-@SuppressWarnings("serial")
+@WebServlet(name="Scaler", urlPatterns={"/Scaler", "/servlet/Scaler/*"}, asyncSupported=true)
 public class Scaler extends HttpServlet {
 
+    private static final long serialVersionUID = 5289386646192471549L;
+
     /** digilib servlet version (for all components) */
-    public static final String version = "1.8.4a2";
+    public static final String version = "1.9.1a16";
 
     /** servlet error codes */
     public static enum Error {UNKNOWN, AUTH, FILE, IMAGE};
@@ -48,31 +49,31 @@
     protected static Logger authlog = Logger.getLogger("digilib.auth");
 
     /** DocuDirCache instance */
-    DocuDirCache dirCache;
+    protected DocuDirCache dirCache;
 
     /** Image executor */
-    DigilibJobCenter<DocuImage> imageJobCenter;
+    protected DigilibJobCenter<DocuImage> imageJobCenter;
 
     /** authentication error image file */
-    File denyImgFile;
+    public static File denyImgFile;
 
     /** image error image file */
-    File errorImgFile;
+    public static File errorImgFile;
 
     /** not found error image file */
-    File notfoundImgFile;
+    public static File notfoundImgFile;
 
     /** send files as is? */
-    boolean sendFileAllowed = true;
+    protected boolean sendFileAllowed = true;
 
     /** DigilibConfiguration instance */
-    DigilibConfiguration dlConfig;
+    protected DigilibConfiguration dlConfig;
 
     /** use authorization database */
-    boolean useAuthorization = true;
+    protected boolean useAuthorization = true;
 
     /** AuthOps instance */
-    AuthOps authOp;
+    protected AuthOps authOp;
 
     /**
      * Initialisation on first run.
@@ -94,8 +95,7 @@
         // get our ServletContext
         ServletContext context = config.getServletContext();
         // see if there is a Configuration instance
-        dlConfig = (DigilibConfiguration) context
-                .getAttribute("digilib.servlet.configuration");
+        dlConfig = (DigilibConfiguration) context.getAttribute("digilib.servlet.configuration");
         if (dlConfig == null) {
             // no Configuration
             throw new ServletException("No Configuration!");
@@ -112,15 +112,16 @@
                 .getValue("servlet.worker.imageexecutor");
 
         denyImgFile = ServletOps.getFile(
-                (File) dlConfig.getValue("denied-image"), config);
+                (File) dlConfig.getValue("denied-image"), context);
         errorImgFile = ServletOps.getFile(
-                (File) dlConfig.getValue("error-image"), config);
+                (File) dlConfig.getValue("error-image"), context);
         notfoundImgFile = ServletOps.getFile(
-                (File) dlConfig.getValue("notfound-image"), config);
+                (File) dlConfig.getValue("notfound-image"), context);
         sendFileAllowed = dlConfig.getAsBoolean("sendfile-allowed");
     }
 
-    /** Returns modification time relevant to the request for caching.
+    /**
+     * Returns modification time relevant to the request for caching.
      * 
      * @see javax.servlet.http.HttpServlet#getLastModified(javax.servlet.http.HttpServletRequest)
      */
@@ -128,13 +129,17 @@
         accountlog.debug("GetLastModified from " + request.getRemoteAddr()
                 + " for " + request.getQueryString());
         long mtime = -1;
-        // create new request
-        DigilibRequest dlReq = new DigilibRequest(request);
-        DocuDirectory dd = dirCache.getDirectory(dlReq.getFilePath());
-        if (dd != null) {
-            mtime = dd.getDirMTime() / 1000 * 1000;
+        try {
+            // create new digilib request
+            DigilibRequest dlReq = new DigilibRequest(request);
+            DocuDirectory dd = dirCache.getDirectory(dlReq.getFilePath());
+            if (dd != null) {
+                mtime = dd.getDirMTime() / 1000 * 1000;
+            }
+        } catch (Exception e) {
+            logger.error("error in getLastModified: " + e.getMessage());
         }
-        logger.debug("  returns "+mtime);
+        logger.debug("  returns " + mtime);
         return mtime;
     }
 
@@ -183,12 +188,14 @@
 
         accountlog.debug("request: " + request.getQueryString());
         logger.debug("request: " + request.getQueryString());
-        long startTime = System.currentTimeMillis();
+        logger.debug("headers: " + ServletOps.headersToString(request));
+        //logger.debug("response:"+ response + " committed=" + response.isCommitted());
+        final long startTime = System.currentTimeMillis();
 
         // parse request
         DigilibRequest dlRequest = new DigilibRequest(request);
         // extract the job information
-        ImageJobDescription jobTicket = ImageJobDescription.getInstance(dlRequest, dlConfig);
+        final ImageJobDescription jobTicket = ImageJobDescription.getInstance(dlRequest, dlConfig);
 
         // type of error reporting
         ErrMsg errMsgType = ErrMsg.IMAGE;
@@ -202,7 +209,7 @@
         	/*
         	 *  check if we can fast-track without scaling
         	 */
-            ImageFile fileToLoad = jobTicket.getFileToLoad();
+            ImageInput fileToLoad = (ImageInput) jobTicket.getInput();
 
             // check permissions
             if (useAuthorization) {
@@ -246,16 +253,14 @@
                 response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
                 return;
             }
+            
+            // worker job is done asynchronously
+            AsyncContext asyncCtx = request.startAsync(request, response); 
             // create job
-            ImageWorker job = new ImageWorker(dlConfig, jobTicket);
+            AsyncServletWorker job = new AsyncServletWorker(dlConfig, jobTicket, asyncCtx, errMsgType, startTime);
             // submit job
-            Future<DocuImage> jobResult = imageJobCenter.submit(job);
-            // wait for result
-            DocuImage img = jobResult.get();
-            // send image
-            ServletOps.sendImage(img, null, response, logger);
-            logger.debug("Job Processing Time: "
-                    + (System.currentTimeMillis() - startTime) + "ms");
+            imageJobCenter.submit(job);
+            // we're done for now
 
         } catch (ImageOpException e) {
             logger.error(e.getClass() + ": " + e.getMessage());
@@ -266,15 +271,11 @@
         } catch (AuthOpException e) {
             logger.error(e.getClass() + ": " + e.getMessage());
             digilibError(errMsgType, Error.AUTH, null, response);
-        } catch (InterruptedException e) {
-            logger.error(e.getClass() + ": " + e.getMessage());
-        } catch (ExecutionException e) {
-            logger.error(e.getClass() + ": " + e.getMessage());
-            String causeMsg = e.getCause().getMessage();
-            logger.error("caused by: " + causeMsg);
-            digilibError(errMsgType, Error.IMAGE, causeMsg, response);
+        } catch (Exception e) {
+            logger.error("Other Exception: ", e);
+            // TODO: should we rethrow or swallow?
+            //throw new ServletException(e);
         }
-
     }
 
     /**
@@ -285,7 +286,7 @@
      * @param msg
      * @param response
      */
-    public void digilibError(ErrMsg type, Error error, String msg,
+    public static void digilibError(ErrMsg type, Error error, String msg,
             HttpServletResponse response) {
         try {
             File img = null;
@@ -306,13 +307,12 @@
                 if (msg == null) {
                     msg = "ERROR: Other image error!";
                 }
-                img = this.errorImgFile;
+                img = errorImgFile;
                 status = HttpServletResponse.SC_BAD_REQUEST;
             }
             if (response.isCommitted()) {
                 // response already committed
-                logger.error("Unable to send error: " + msg);
-                return;
+                logger.warn("Response committed for error "+msg);
             }
             if (type == ErrMsg.TEXT) {
                 ServletOps.htmlMessage(msg, response);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/servlet/ScalerNoAsync.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,346 @@
+package digilib.servlet;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
+
+import digilib.auth.AuthOpException;
+import digilib.auth.AuthOps;
+import digilib.image.DocuImage;
+import digilib.image.ImageJobDescription;
+import digilib.image.ImageOpException;
+import digilib.image.ImageWorker;
+import digilib.io.DocuDirCache;
+import digilib.io.DocuDirectory;
+import digilib.io.ImageInput;
+import digilib.util.DigilibJobCenter;
+
+/**
+ * Version of Scaler servlet that uses a thread pool but not Servlet 3.0 async API. 
+ */
+public class ScalerNoAsync extends HttpServlet {
+
+    private static final long serialVersionUID = -5439198888139362735L;
+
+    /** digilib servlet version (for all components) */
+    public static final String version = "1.9.0a5 noasync";
+
+    /** servlet error codes */
+    public static enum Error {UNKNOWN, AUTH, FILE, IMAGE};
+    
+    /** type of error message */
+    public static enum ErrMsg {IMAGE, TEXT, CODE};
+    
+    /** logger for accounting requests */
+    protected static Logger accountlog = Logger.getLogger("account.request");
+
+    /** gengeral logger for this class */
+    protected static Logger logger = Logger.getLogger("digilib.scaler");
+
+    /** logger for authentication related */
+    protected static Logger authlog = Logger.getLogger("digilib.auth");
+
+    /** DocuDirCache instance */
+    protected DocuDirCache dirCache;
+
+    /** Image executor */
+    DigilibJobCenter<DocuImage> imageJobCenter;
+
+    /** authentication error image file */
+    public static File denyImgFile;
+
+    /** image error image file */
+    public static File errorImgFile;
+
+    /** not found error image file */
+    public static File notfoundImgFile;
+
+    /** send files as is? */
+    protected boolean sendFileAllowed = true;
+
+    /** DigilibConfiguration instance */
+    protected DigilibConfiguration dlConfig;
+
+    /** use authorization database */
+    protected boolean useAuthorization = true;
+
+    /** AuthOps instance */
+    protected AuthOps authOp;
+
+    /**
+     * Initialisation on first run.
+     * 
+     * @throws ServletException
+     * 
+     * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
+     */
+    public void init(ServletConfig config) throws ServletException {
+        super.init(config);
+
+        System.out
+                .println("***** Digital Image Library Image Scaler Servlet (version "
+                        + version + ") *****");
+        // say hello in the log file
+        logger.info("***** Digital Image Library Image Scaler Servlet (version "
+                + version + ") *****");
+
+        // get our ServletContext
+        ServletContext context = config.getServletContext();
+        // see if there is a Configuration instance
+        dlConfig = (DigilibConfiguration) context
+                .getAttribute("digilib.servlet.configuration");
+        if (dlConfig == null) {
+            // no Configuration
+            throw new ServletException("No Configuration!");
+        }
+        // set our AuthOps
+        useAuthorization = dlConfig.getAsBoolean("use-authorization");
+        authOp = (AuthOps) dlConfig.getValue("servlet.auth.op");
+
+        // DocuDirCache instance
+        dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
+
+        // Executor
+        imageJobCenter = (DigilibJobCenter<DocuImage>) dlConfig
+                .getValue("servlet.worker.imageexecutor");
+
+        denyImgFile = ServletOps.getFile(
+                (File) dlConfig.getValue("denied-image"), context);
+        errorImgFile = ServletOps.getFile(
+                (File) dlConfig.getValue("error-image"), context);
+        notfoundImgFile = ServletOps.getFile(
+                (File) dlConfig.getValue("notfound-image"), context);
+        sendFileAllowed = dlConfig.getAsBoolean("sendfile-allowed");
+    }
+
+    /** Returns modification time relevant to the request for caching.
+     * 
+     * @see javax.servlet.http.HttpServlet#getLastModified(javax.servlet.http.HttpServletRequest)
+     */
+    public long getLastModified(HttpServletRequest request) {
+        accountlog.debug("GetLastModified from " + request.getRemoteAddr()
+                + " for " + request.getQueryString());
+        long mtime = -1;
+        // create new request
+        DigilibRequest dlReq = new DigilibRequest(request);
+        DocuDirectory dd = dirCache.getDirectory(dlReq.getFilePath());
+        if (dd != null) {
+            mtime = dd.getDirMTime() / 1000 * 1000;
+        }
+        logger.debug("  returns "+mtime);
+        return mtime;
+    }
+
+    /* (non-Javadoc)
+     * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException {
+        accountlog.info("GET from " + request.getRemoteAddr());
+        this.processRequest(request, response);
+    }
+
+
+    /* (non-Javadoc)
+     * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException {
+        accountlog.info("POST from " + request.getRemoteAddr());
+        this.processRequest(request, response);
+    }
+    
+
+	protected void doHead(HttpServletRequest req, HttpServletResponse resp)
+			throws ServletException, IOException {
+		logger.debug("HEAD from "+req.getRemoteAddr());
+		super.doHead(req, resp);
+	}
+
+	protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
+			throws ServletException, IOException {
+		logger.debug("OPTIONS from "+req.getRemoteAddr());
+		super.doOptions(req, resp);
+	}
+
+	/** Service this request using the response.
+     * @param request
+     * @param response
+     * @throws ServletException 
+     */
+    public void processRequest(HttpServletRequest request,
+            HttpServletResponse response) throws ServletException {
+
+        if (dlConfig == null) {
+            logger.error("ERROR: No Configuration!");
+            throw new ServletException("NO VALID digilib CONFIGURATION!");
+        }
+
+        accountlog.debug("request: " + request.getQueryString());
+        logger.debug("request: " + request.getQueryString());
+        long startTime = System.currentTimeMillis();
+
+        // parse request
+        DigilibRequest dlRequest = new DigilibRequest(request);
+        // extract the job information
+        ImageJobDescription jobTicket = ImageJobDescription.getInstance(dlRequest, dlConfig);
+
+        // type of error reporting
+        ErrMsg errMsgType = ErrMsg.IMAGE;
+        if (dlRequest.hasOption("errtxt")) {
+        	errMsgType = ErrMsg.TEXT;
+        } else if (dlRequest.hasOption("errcode")) {
+        	errMsgType = ErrMsg.CODE;
+        }
+        
+        try {
+        	/*
+        	 *  check if we can fast-track without scaling
+        	 */
+            ImageInput fileToLoad = (ImageInput) jobTicket.getInput();
+
+            // check permissions
+            if (useAuthorization) {
+                // get a list of required roles (empty if no restrictions)
+                List<String> rolesRequired = authOp.rolesForPath(
+                        jobTicket.getFilePath(), request);
+                if (rolesRequired != null) {
+                    authlog.debug("Role required: " + rolesRequired);
+                    authlog.debug("User: " + request.getRemoteUser());
+                    // is the current request/user authorized?
+                    if (!authOp.isRoleAuthorized(rolesRequired, request)) {
+                        // send deny answer and abort
+                        throw new AuthOpException();
+                    }
+                }
+            }
+
+            // if requested, send image as a file
+            if (sendFileAllowed && jobTicket.getSendAsFile()) {
+                String mt = null;
+                if (jobTicket.hasOption("rawfile")) {
+                    mt = "application/octet-stream";
+                }
+                logger.debug("Sending RAW File as is.");
+                ServletOps.sendFile(fileToLoad.getFile(), mt, null, response, logger);
+                logger.info("Done in " + (System.currentTimeMillis() - startTime) + "ms");
+                return;
+            }
+
+            // if possible, send the image without actually having to transform it
+            if (! jobTicket.isTransformRequired()) {
+                logger.debug("Sending File as is.");
+                ServletOps.sendFile(fileToLoad.getFile(), null, null, response, logger);
+                logger.info("Done in " + (System.currentTimeMillis() - startTime) + "ms");
+                return;
+            }
+
+            // check load of workers
+            if (imageJobCenter.isBusy()) {
+                logger.error("Servlet overloaded!");
+                response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+                return;
+            }
+            // create job
+            ImageWorker job = new ImageWorker(dlConfig, jobTicket);
+            // submit job
+            Future<DocuImage> jobResult = imageJobCenter.submit(job);
+            // wait for result
+            DocuImage img = jobResult.get();
+            // forced destination image type
+            String mt = null;
+            if (jobTicket.hasOption("jpg")) {
+            	mt = "image/jpeg";
+            } else if (jobTicket.hasOption("png")) {
+            	mt = "image/png";
+            }
+            // send image
+            ServletOps.sendImage(img, mt, response, logger);
+            logger.debug("Job Processing Time: "
+                    + (System.currentTimeMillis() - startTime) + "ms");
+
+        } catch (ImageOpException e) {
+            logger.error(e.getClass() + ": " + e.getMessage());
+            digilibError(errMsgType, Error.IMAGE, null, response);
+        } catch (IOException e) {
+            logger.error(e.getClass() + ": " + e.getMessage());
+            digilibError(errMsgType, Error.FILE, null, response);
+        } catch (AuthOpException e) {
+            logger.error(e.getClass() + ": " + e.getMessage());
+            digilibError(errMsgType, Error.AUTH, null, response);
+        } catch (InterruptedException e) {
+            logger.error(e.getClass() + ": " + e.getMessage());
+        } catch (ExecutionException e) {
+            logger.error(e.getClass() + ": " + e.getMessage());
+            String causeMsg = e.getCause().getMessage();
+            logger.error("caused by: " + causeMsg);
+            digilibError(errMsgType, Error.IMAGE, causeMsg, response);
+        }
+
+    }
+
+    /**
+     * Sends an error to the client as text or image.
+     * 
+     * @param type
+     * @param error
+     * @param msg
+     * @param response
+     */
+    public static void digilibError(ErrMsg type, Error error, String msg,
+            HttpServletResponse response) {
+        try {
+            File img = null;
+            int status = 0;
+            if (error == Error.AUTH) {
+                if (msg == null) {
+                    msg = "ERROR: Unauthorized access!";
+                }
+                img = denyImgFile;
+                status = HttpServletResponse.SC_FORBIDDEN;
+            } else if (error == Error.FILE) {
+                if (msg == null) {
+                    msg = "ERROR: Image file not found!";
+                }
+                img = notfoundImgFile;
+                status = HttpServletResponse.SC_NOT_FOUND;
+            } else {
+                if (msg == null) {
+                    msg = "ERROR: Other image error!";
+                }
+                img = errorImgFile;
+                status = HttpServletResponse.SC_BAD_REQUEST;
+            }
+            if (response.isCommitted()) {
+                // response already committed
+                logger.error("Unable to send error: " + msg);
+                return;
+            }
+            if (type == ErrMsg.TEXT) {
+                ServletOps.htmlMessage(msg, response);
+            } else if (type == ErrMsg.CODE) {
+                response.sendError(status, msg);
+            } else if (img != null) {
+                // default: image
+                ServletOps.sendFile(img, null, null, response, logger);
+            }
+        } catch (Exception e) {
+            logger.error("Error sending error!", e);
+        }
+
+    }
+
+    public static String getVersion() {
+        return version;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/servlet/ScalerNoThread.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,321 @@
+package digilib.servlet;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
+
+import digilib.auth.AuthOpException;
+import digilib.auth.AuthOps;
+import digilib.image.DocuImage;
+import digilib.image.ImageJobDescription;
+import digilib.image.ImageOpException;
+import digilib.image.ImageWorker;
+import digilib.io.DocuDirCache;
+import digilib.io.DocuDirectory;
+import digilib.io.ImageInput;
+
+/**
+ * Version of Scaler servlet that doesn't use a thread pool.
+ */
+public class ScalerNoThread extends HttpServlet {
+
+    private static final long serialVersionUID = 1450947819851623306L;
+
+    /** digilib servlet version (for all components) */
+    public static final String version = "1.9.0a5 nothread";
+
+    /** servlet error codes */
+    public static enum Error {UNKNOWN, AUTH, FILE, IMAGE};
+    
+    /** type of error message */
+    public static enum ErrMsg {IMAGE, TEXT, CODE};
+    
+    /** logger for accounting requests */
+    protected static Logger accountlog = Logger.getLogger("account.request");
+
+    /** gengeral logger for this class */
+    protected static Logger logger = Logger.getLogger("digilib.scaler");
+
+    /** logger for authentication related */
+    protected static Logger authlog = Logger.getLogger("digilib.auth");
+
+    /** DocuDirCache instance */
+    protected DocuDirCache dirCache;
+
+    /** authentication error image file */
+    public static File denyImgFile;
+
+    /** image error image file */
+    public static File errorImgFile;
+
+    /** not found error image file */
+    public static File notfoundImgFile;
+
+    /** send files as is? */
+    protected boolean sendFileAllowed = true;
+
+    /** DigilibConfiguration instance */
+    protected DigilibConfiguration dlConfig;
+
+    /** use authorization database */
+    protected boolean useAuthorization = true;
+
+    /** AuthOps instance */
+    protected AuthOps authOp;
+
+    /**
+     * Initialisation on first run.
+     * 
+     * @throws ServletException
+     * 
+     * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
+     */
+    public void init(ServletConfig config) throws ServletException {
+        super.init(config);
+
+        System.out
+                .println("***** Digital Image Library Image Scaler Servlet (version "
+                        + version + ") *****");
+        // say hello in the log file
+        logger.info("***** Digital Image Library Image Scaler Servlet (version "
+                + version + ") *****");
+
+        // get our ServletContext
+        ServletContext context = config.getServletContext();
+        // see if there is a Configuration instance
+        dlConfig = (DigilibConfiguration) context
+                .getAttribute("digilib.servlet.configuration");
+        if (dlConfig == null) {
+            // no Configuration
+            throw new ServletException("No Configuration!");
+        }
+        // set our AuthOps
+        useAuthorization = dlConfig.getAsBoolean("use-authorization");
+        authOp = (AuthOps) dlConfig.getValue("servlet.auth.op");
+
+        // DocuDirCache instance
+        dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
+
+        denyImgFile = ServletOps.getFile(
+                (File) dlConfig.getValue("denied-image"), context);
+        errorImgFile = ServletOps.getFile(
+                (File) dlConfig.getValue("error-image"), context);
+        notfoundImgFile = ServletOps.getFile(
+                (File) dlConfig.getValue("notfound-image"), context);
+        sendFileAllowed = dlConfig.getAsBoolean("sendfile-allowed");
+    }
+
+    /** Returns modification time relevant to the request for caching.
+     * 
+     * @see javax.servlet.http.HttpServlet#getLastModified(javax.servlet.http.HttpServletRequest)
+     */
+    public long getLastModified(HttpServletRequest request) {
+        accountlog.debug("GetLastModified from " + request.getRemoteAddr()
+                + " for " + request.getQueryString());
+        long mtime = -1;
+        // create new request
+        DigilibRequest dlReq = new DigilibRequest(request);
+        DocuDirectory dd = dirCache.getDirectory(dlReq.getFilePath());
+        if (dd != null) {
+            mtime = dd.getDirMTime() / 1000 * 1000;
+        }
+        logger.debug("  returns "+mtime);
+        return mtime;
+    }
+
+    /* (non-Javadoc)
+     * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException {
+        accountlog.info("GET from " + request.getRemoteAddr());
+        this.processRequest(request, response);
+    }
+
+
+    /* (non-Javadoc)
+     * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException {
+        accountlog.info("POST from " + request.getRemoteAddr());
+        this.processRequest(request, response);
+    }
+    
+
+	protected void doHead(HttpServletRequest req, HttpServletResponse resp)
+			throws ServletException, IOException {
+		logger.debug("HEAD from "+req.getRemoteAddr());
+		super.doHead(req, resp);
+	}
+
+	protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
+			throws ServletException, IOException {
+		logger.debug("OPTIONS from "+req.getRemoteAddr());
+		super.doOptions(req, resp);
+	}
+
+	/** Service this request using the response.
+     * @param request
+     * @param response
+     * @throws ServletException 
+     */
+    public void processRequest(HttpServletRequest request,
+            HttpServletResponse response) throws ServletException {
+
+        if (dlConfig == null) {
+            logger.error("ERROR: No Configuration!");
+            throw new ServletException("NO VALID digilib CONFIGURATION!");
+        }
+
+        accountlog.debug("request: " + request.getQueryString());
+        logger.debug("request: " + request.getQueryString());
+        long startTime = System.currentTimeMillis();
+
+        // parse request
+        DigilibRequest dlRequest = new DigilibRequest(request);
+        // extract the job information
+        ImageJobDescription jobTicket = ImageJobDescription.getInstance(dlRequest, dlConfig);
+
+        // type of error reporting
+        ErrMsg errMsgType = ErrMsg.IMAGE;
+        if (dlRequest.hasOption("errtxt")) {
+        	errMsgType = ErrMsg.TEXT;
+        } else if (dlRequest.hasOption("errcode")) {
+        	errMsgType = ErrMsg.CODE;
+        }
+        
+        try {
+        	/*
+        	 *  check if we can fast-track without scaling
+        	 */
+            ImageInput fileToLoad = (ImageInput) jobTicket.getInput();
+
+            // check permissions
+            if (useAuthorization) {
+                // get a list of required roles (empty if no restrictions)
+                List<String> rolesRequired = authOp.rolesForPath(
+                        jobTicket.getFilePath(), request);
+                if (rolesRequired != null) {
+                    authlog.debug("Role required: " + rolesRequired);
+                    authlog.debug("User: " + request.getRemoteUser());
+                    // is the current request/user authorized?
+                    if (!authOp.isRoleAuthorized(rolesRequired, request)) {
+                        // send deny answer and abort
+                        throw new AuthOpException();
+                    }
+                }
+            }
+
+            // if requested, send image as a file
+            if (sendFileAllowed && jobTicket.getSendAsFile()) {
+                String mt = null;
+                if (jobTicket.hasOption("rawfile")) {
+                    mt = "application/octet-stream";
+                }
+                logger.debug("Sending RAW File as is.");
+                ServletOps.sendFile(fileToLoad.getFile(), mt, null, response, logger);
+                logger.info("Done in " + (System.currentTimeMillis() - startTime) + "ms");
+                return;
+            }
+
+            // if possible, send the image without actually having to transform it
+            if (! jobTicket.isTransformRequired()) {
+                logger.debug("Sending File as is.");
+                ServletOps.sendFile(fileToLoad.getFile(), null, null, response, logger);
+                logger.info("Done in " + (System.currentTimeMillis() - startTime) + "ms");
+                return;
+            }
+
+            // create job
+            ImageWorker job = new ImageWorker(dlConfig, jobTicket);
+            // get result immediately
+            DocuImage img = job.call();
+            // forced destination image type
+            String mt = null;
+            if (jobTicket.hasOption("jpg")) {
+            	mt = "image/jpeg";
+            } else if (jobTicket.hasOption("png")) {
+            	mt = "image/png";
+            }
+            // send image
+            ServletOps.sendImage(img, mt, response, logger);
+            logger.debug("Job Processing Time: "
+                    + (System.currentTimeMillis() - startTime) + "ms");
+
+        } catch (ImageOpException e) {
+            logger.error(e.getClass() + ": " + e.getMessage());
+            digilibError(errMsgType, Error.IMAGE, null, response);
+        } catch (IOException e) {
+            logger.error(e.getClass() + ": " + e.getMessage());
+            digilibError(errMsgType, Error.FILE, null, response);
+        } catch (AuthOpException e) {
+            logger.error(e.getClass() + ": " + e.getMessage());
+            digilibError(errMsgType, Error.AUTH, null, response);
+        }
+
+    }
+
+    /**
+     * Sends an error to the client as text or image.
+     * 
+     * @param type
+     * @param error
+     * @param msg
+     * @param response
+     */
+    public static void digilibError(ErrMsg type, Error error, String msg,
+            HttpServletResponse response) {
+        try {
+            File img = null;
+            int status = 0;
+            if (error == Error.AUTH) {
+                if (msg == null) {
+                    msg = "ERROR: Unauthorized access!";
+                }
+                img = denyImgFile;
+                status = HttpServletResponse.SC_FORBIDDEN;
+            } else if (error == Error.FILE) {
+                if (msg == null) {
+                    msg = "ERROR: Image file not found!";
+                }
+                img = notfoundImgFile;
+                status = HttpServletResponse.SC_NOT_FOUND;
+            } else {
+                if (msg == null) {
+                    msg = "ERROR: Other image error!";
+                }
+                img = errorImgFile;
+                status = HttpServletResponse.SC_BAD_REQUEST;
+            }
+            if (response.isCommitted()) {
+                // response already committed
+                logger.error("Unable to send error: " + msg);
+                return;
+            }
+            if (type == ErrMsg.TEXT) {
+                ServletOps.htmlMessage(msg, response);
+            } else if (type == ErrMsg.CODE) {
+                response.sendError(status, msg);
+            } else if (img != null) {
+                // default: image
+                ServletOps.sendFile(img, null, null, response, logger);
+            }
+        } catch (Exception e) {
+            logger.error("Error sending error!", e);
+        }
+
+    }
+
+    public static String getVersion() {
+        return version;
+    }
+
+}
--- a/servlet/src/digilib/servlet/ServletOps.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/servlet/ServletOps.java	Tue Mar 01 22:00:50 2011 +0100
@@ -26,10 +26,14 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Enumeration;
 import java.util.StringTokenizer;
 
-import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.log4j.Logger;
@@ -72,11 +76,11 @@
      * @param sc
      * @return
      */
-    public static File getFile(File f, ServletConfig sc) {
+    public static File getFile(File f, ServletContext sc) {
         // is the filename absolute?
         if (!f.isAbsolute()) {
             // relative path -> use getRealPath to resolve in WEB-INF
-            String fn = sc.getServletContext().getRealPath(f.getPath());
+            String fn = sc.getRealPath(f.getPath());
             f = new File(fn);
         }
         return f;
@@ -92,12 +96,12 @@
      * @param sc
      * @return
      */
-    public static String getFile(String filename, ServletConfig sc) {
+    public static String getFile(String filename, ServletContext sc) {
         File f = new File(filename);
         // is the filename absolute?
         if (!f.isAbsolute()) {
             // relative path -> use getRealPath to resolve in WEB-INF
-            filename = sc.getServletContext().getRealPath(filename);
+            filename = sc.getRealPath(filename);
         }
         return filename;
     }
@@ -112,7 +116,7 @@
      * @param sc
      * @return
      */
-    public static File getConfigFile(File f, ServletConfig sc) {
+    public static File getConfigFile(File f, ServletContext sc) {
         String fn = f.getPath();
         // is the filename absolute?
         if (f.isAbsolute()) {
@@ -126,7 +130,7 @@
             }
         }
         // relative path -> use getRealPath to resolve in WEB-INF
-        String newfn = sc.getServletContext().getRealPath("WEB-INF/" + fn);
+        String newfn = sc.getRealPath("WEB-INF/" + fn);
         f = new File(newfn);
         return f;
     }
@@ -141,19 +145,18 @@
      * @param sc
      * @return
      */
-    public static String getConfigFile(String filename, ServletConfig sc) {
+    public static String getConfigFile(String filename, ServletContext sc) {
         File f = new File(filename);
         // is the filename absolute?
         if (!f.isAbsolute()) {
             // relative path -> use getRealPath to resolve in WEB-INF
-            filename = sc.getServletContext()
-                    .getRealPath("WEB-INF/" + filename);
+            filename = sc.getRealPath("WEB-INF/" + filename);
         }
         return filename;
     }
 
     /**
-     * print a servlet response and exit
+     * print a servlet response
      */
     public static void htmlMessage(String msg, HttpServletResponse response)
             throws IOException {
@@ -161,7 +164,7 @@
     }
 
     /**
-     * print a servlet response and exit
+     * print a servlet response
      */
     public static void htmlMessage(String title, String msg,
             HttpServletResponse response) throws IOException {
@@ -192,10 +195,10 @@
      * @throws ImageOpException
      * @throws ServletException
      *             Exception on sending data.
+     * @throws IOException
      */
     public static void sendFile(File f, String mt, String name,
-            HttpServletResponse response) throws ImageOpException,
-            ServletException {
+            HttpServletResponse response) throws ImageOpException, IOException {
         // use default logger
         ServletOps.sendFile(f, mt, name, response, ServletOps.logger);
     }
@@ -218,13 +221,14 @@
      *            Logger to use
      * @throws ImageOpException
      * @throws ServletException Exception on sending data.
+     * @throws IOException 
      */
     public static void sendFile(File f, String mt, String name, HttpServletResponse response, Logger logger)
-            throws ImageOpException, ServletException {
+            throws ImageOpException, IOException {
         logger.debug("sendRawFile(" + mt + ", " + f + ")");
-    	if (response.isCommitted()) {
-        	logger.warn("sendFile: response already committed!");
-        	//return;
+    	if (response == null) {
+    		logger.error("No response!");
+    		return;
     	}
         if (mt == null) {
             // auto-detect mime-type
@@ -255,9 +259,6 @@
                 // copy out file
                 outStream.write(dataBuffer, 0, len);
             }
-        } catch (IOException e) {
-            logger.error("Error sending file:", e);
-            throw new ServletException("Error sending file:", e);
         } finally {
             try {
                 if (inFile != null) {
@@ -297,19 +298,26 @@
     public static void sendImage(DocuImage img, String mimeType,
             HttpServletResponse response, Logger logger) throws ImageOpException,
             ServletException {
-    	if (response.isCommitted()) {
-        	logger.warn("sendImage: response already committed!");
-        	//return;
+    	if (response == null) {
+    		logger.error("No response!");
+    		return;
     	}
+        logger.debug("sending to response: ("+ headersToString(response) + ") committed=" + response.isCommitted());
+        // TODO: should we erase or replace old last-modified header?
         try {
             OutputStream outstream = response.getOutputStream();
             // setup output -- if mime type is set use that otherwise
             // if source is JPG then dest will be JPG else it's PNG
             if (mimeType == null) {
                 mimeType = img.getMimetype();
+                if (mimeType == null) {
+                    // still no mime-type
+                    logger.warn("sendImage without mime-type! using image/jpeg.");
+                    mimeType = "image/jpeg";
+                }
             }
-            if ((mimeType.equals("image/jpeg") || mimeType.equals("image/jp2") || mimeType
-                    .equals("image/fpx"))) {
+            if ((mimeType.equals("image/jpeg") || mimeType.equals("image/jp2") || 
+                    mimeType.equals("image/fpx"))) {
                 mimeType = "image/jpeg";
             } else {
                 mimeType = "image/png";
@@ -318,10 +326,36 @@
             response.setContentType(mimeType);
             img.writeImage(mimeType, outstream);
         } catch (IOException e) {
-            logger.error("Error sending image:", e);
             throw new ServletException("Error sending image:", e);
         }
         // TODO: should we: finally { img.dispose(); }
     }
 
+    /** Returns text representation of headers for debuggging purposes.
+     * @param req
+     * @return
+     */
+    public static String headersToString(HttpServletRequest req) {
+        String s = "";
+        Enumeration<String> hns = req.getHeaderNames();
+        while (hns.hasMoreElements()) {
+            String hn = hns.nextElement();
+            s += hn + "=" + req.getHeader(hn) + "; ";
+        }
+        return s;
+    }
+    
+    /** Returns text representation of headers for debuggging purposes.
+     * @param resp
+     * @return
+     */
+    public static String headersToString(HttpServletResponse resp) {
+        String s = "";
+        Collection<String> hns = resp.getHeaderNames();
+        for (String hn : hns) {
+            s += hn + "=" + resp.getHeader(hn) + "; ";
+        }
+        return s;
+    }
+    
 }
\ No newline at end of file
--- a/servlet/src/digilib/servlet/Texter.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/servlet/Texter.java	Tue Mar 01 22:00:50 2011 +0100
@@ -30,7 +30,6 @@
 import digilib.auth.AuthOps;
 import digilib.image.ImageOpException;
 import digilib.io.DocuDirCache;
-import digilib.io.FileOpException;
 import digilib.io.FileOps;
 import digilib.io.FileOps.FileClass;
 import digilib.io.TextFile;
@@ -44,8 +43,10 @@
  */
 public class Texter extends HttpServlet {
 
-	/** Servlet version */
-	public static String tlVersion = "0.1b2";
+    private static final long serialVersionUID = 6678666342141409867L;
+
+    /** Servlet version */
+	public static String tlVersion = "0.1b3";
 
 	/** DigilibConfiguration instance */
 	DigilibConfiguration dlConfig = null;
@@ -128,36 +129,42 @@
 		processRequest(request, response);
 	}
 
-    protected void processRequest(HttpServletRequest request,
-            HttpServletResponse response) throws ServletException, IOException {
-
-        /*
-         * request parameters
-         */
+	protected void processRequest(HttpServletRequest request,
+			HttpServletResponse response) {
+		
+		/*
+		 * request parameters
+		 */
         // create new request with defaults
         DigilibRequest dlRequest = new DigilibRequest(request);
-        try {
-            /*
-             * find the file to load/send
-             */
-            TextFile f = getTextFile(dlRequest, "/txt");
-            if (f != null) {
-                ServletOps.sendFile(f.getFile(), null, null, response, logger);
-            } else {
-                f = getTextFile(dlRequest, "");
-                if (f != null) {
-                    ServletOps.sendFile(f.getFile(), null, null, response, logger);
-                } else {
-                    response.sendError(HttpServletResponse.SC_NOT_FOUND,
-                            "Text-File not found!");
-                    // ServletOps.htmlMessage("No Text-File!", response);
-                }
-            }
-
-        } catch (ImageOpException e) {
+		try {
+			
+			/*
+			 * find the file to load/send
+			 */
+			TextFile f = getTextFile(dlRequest, "/txt");
+			if (f != null) {
+				ServletOps.sendFile(f.getFile(), null, null, response, logger);
+			} else {
+				f = getTextFile(dlRequest, "");
+				if (f != null) {
+					ServletOps.sendFile(f.getFile(), null, null, response, logger);
+				} else {
+					response.sendError(HttpServletResponse.SC_NOT_FOUND, "Text-File not found!");
+					//ServletOps.htmlMessage("No Text-File!", response);
+				}
+			}
+			
+		} catch (ImageOpException e) {
             // most likely wrong file format...
             logger.error("ERROR sending text file: ", e);
-            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+            try {
+                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+            } catch (IOException e1) {
+                logger.error("ERROR sending error: ", e1);
+            }
+        } catch (IOException e) {
+            logger.error("ERROR sending text file: ", e);
         }
     }
 	
@@ -176,7 +183,7 @@
 	private TextFile getTextFile(DigilibRequest dlRequest, String subDirectory) {
 		String loadPathName = dlRequest.getFilePath() + subDirectory;
 		// find the file(set)
-		return (TextFile) dirCache.getFile(loadPathName, dlRequest
-				.getAsInt("pn"), FileClass.TEXT);
+		return (TextFile) dirCache.getFile(loadPathName, dlRequest.getAsInt("pn"), 
+		        FileClass.TEXT);
 	}
 }
\ No newline at end of file
--- a/servlet/src/digilib/util/DigilibJobCenter.java	Tue Mar 01 17:12:25 2011 +0100
+++ b/servlet/src/digilib/util/DigilibJobCenter.java	Tue Mar 01 22:00:50 2011 +0100
@@ -51,7 +51,7 @@
         }
     }
     
-    /** Submit job to execute
+    /** Submit Callable job that returns a Value to execute.
      * 
      * @param job
      * @return Future to control the job
@@ -60,6 +60,15 @@
         return executor.submit(job);
     }
 
+    /** Submit Runnable job to execute.
+     * 
+     * @param job
+     * @return Future to control the job
+     */
+    public Future<?> submit(Runnable job) {
+        return executor.submit(job);
+    }
+
     /** Returns if the service is overloaded.
      *  
      * @return
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/util/HashTree.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,113 @@
+/*  HashTree -- Tree in a Hashtable
+
+ Digital Image Library servlet components
+
+ Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
+
+ This program is free software; you can redistribute  it and/or modify it
+ under  the terms of  the GNU General  Public License as published by the
+ Free Software Foundation;  either version 2 of the  License, or (at your
+ option) any later version.
+ 
+ Please read license.txt for the full details. A copy of the GPL
+ may be found at http://www.gnu.org/copyleft/lgpl.html
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+ */
+
+package digilib.util;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+/**
+ * Tree representation wrapper for a HashMap.
+ * 
+ * The HashTree is constructed from a HashMap filled with 'branches' with
+ * 'leaves'. The branches are stored as String keys in the HashMap. The String
+ * values are leaves.
+ * 
+ * Branches are matched in 'twigs' separated by 'twig separator' Strings. The
+ * return values for a match are leaf values separated by 'leaf separator'
+ * Strings.
+ * 
+ * @author casties
+ */
+public class HashTree {
+
+    private Map<String, String> table;
+
+    private String twigSep = "/";
+
+    private String leafSep = ",";
+
+    /**
+     * Constructor of a HashTree.
+     * 
+     * Creates a HashTree wrapper around a given HashMap, using the given twig
+     * separator and leaf separator.
+     * 
+     * @param t
+     * @param twig_separator
+     * @param leaf_separator
+     */
+    public HashTree(Map<String, String> t, String twig_separator, String leaf_separator) {
+        table = t;
+        twigSep = twig_separator;
+        leafSep = leaf_separator;
+        optimizeTable();
+    }
+
+    private void optimizeTable() {
+    }
+
+    /**
+     * Matches the given branch against the HashTree.
+     * 
+     * Returns a LinkedList of all leaves on all matching branches in the tree.
+     * Branches in the tree match if they are substrings starting at the same
+     * root.
+     * 
+     * @param branch
+     * @return
+     */
+    public List<String> match(String branch) {
+        String b = "";
+        String m;
+        LinkedList<String> matches = new LinkedList<String>();
+
+        // split branch
+        StringTokenizer twig = new StringTokenizer(branch, twigSep);
+        // walk branch and check with tree
+        while (twig.hasMoreTokens()) {
+            if (b.length() == 0) {
+                b = twig.nextToken();
+            } else {
+                b += twigSep + twig.nextToken();
+            }
+            m = table.get(b);
+            if (m != null) {
+                if (m.indexOf(leafSep) < 0) {
+                    // single leaf
+                    matches.add(m);
+                } else {
+                    // split leaves
+                    StringTokenizer leaf = new StringTokenizer(m, leafSep);
+                    while (leaf.hasMoreTokens()) {
+                        matches.add(leaf.nextToken());
+                    }
+                }
+            }
+        }
+        if (matches.size() > 0) {
+            return matches;
+        } else {
+            return null;
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/util/ImageSize.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,230 @@
+/*
+ * ImageSize.java -- digilib image size class. 
+ * Digital Image Library servlet components 
+ * Copyright (C) 2003 Robert Casties (robcast@mail.berlios.de) 
+ * This program is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 2 of the License, or (at your option) any later
+ * version. Please read license.txt for the full details. A copy of the GPL may
+ * be found at http://www.gnu.org/copyleft/lgpl.html You should have received a
+ * copy of the GNU General Public License along with this program; if not,
+ * write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307 USA Created on 26.08.2003
+ */
+
+package digilib.util;
+
+/** Class for image size (width, height).
+ * 
+ * A width or height of 0 is treated as a 'wildcard' that matches any size.
+ * 
+ * @author casties
+ *          
+ */
+public class ImageSize {
+	public int width;
+	public int height;
+
+	public ImageSize() {
+		super();
+	}
+
+	public ImageSize(int width, int height) {
+		this.width = width;
+		this.height = height;
+	}
+
+	public ImageSize(ImageSize i) {
+		this.width = i.width;
+		this.height = i.height;
+	}
+
+	public void setSize(int width, int height) {
+		this.width = width;
+		this.height = height;
+	}
+
+	/**
+	 * Returns if the size of this image is smaller in every dimension than the
+	 * other image.
+	 * 
+	 * 
+	 * 
+	 * @param is
+	 * @return
+	 */
+	public boolean isTotallySmallerThan(ImageSize is) {
+		if ((this.width == 0)||(is.width == 0)) {
+			// width wildcard
+			return (this.height <= is.height);
+		}
+		if ((this.height == 0)||(is.height == 0)) {
+			// height wildcard
+			return (this.width <= is.width);
+		}
+		return ((this.width <= is.width)&&(this.height <= is.height));
+	}
+
+	/**
+	 * Returns if the size of this image is smaller in at least one dimension
+	 * than the other image.
+	 * 
+	 * @param is
+	 * @return
+	 */
+	public boolean isSmallerThan(ImageSize is) {
+		if ((this.width == 0)||(is.width == 0)) {
+			// width wildcard
+			return (this.height <= is.height);
+		}
+		if ((this.height == 0)||(is.height == 0)) {
+			// height wildcard
+			return (this.width <= is.width);
+		}
+		return ((this.width <= is.width) || (this.height <= is.height));
+	}
+
+	/**
+	 * Returns if the size of this image is bigger in every dimension than the
+	 * other image.
+	 * 
+	 * 
+	 * 
+	 * @param is
+	 * @return
+	 */
+	public boolean isTotallyBiggerThan(ImageSize is) {
+		if ((this.width == 0)||(is.width == 0)) {
+			// width wildcard
+			return (this.height >= is.height);
+		}
+		if ((this.height == 0)||(is.height == 0)) {
+			// height wildcard
+			return (this.width >= is.width);
+		}
+		return ((this.width >= is.width) && (this.height >= is.height));
+	}
+
+	/**
+	 * Returns if the size of this image is bigger in at least one dimension
+	 * than the other image.
+	 * 
+	 * 
+	 * 
+	 * @param is
+	 * @return
+	 */
+	public boolean isBiggerThan(ImageSize is) {
+		if ((this.width == 0)||(is.width == 0)) {
+			// width wildcard
+			return (this.height >= is.height);
+		}
+		if ((this.height == 0)||(is.height == 0)) {
+			// height wildcard
+			return (this.width >= is.width);
+		}
+		return ((this.width >= is.width) || (this.height >= is.height));
+	}
+
+	/**
+	 * Returns if this image has the same size or height as the other image.
+	 * 
+	 * 
+	 * 
+	 * @param is
+	 * @return
+	 */
+	public boolean fitsIn(ImageSize is) {
+		if ((this.width == 0)||(is.width == 0)) {
+			// width wildcard
+			return (this.height == is.height);
+		}
+		if ((this.height == 0)||(is.height == 0)) {
+			// height wildcard
+			return (this.width == is.width);
+		}
+		return (
+			(this.width == is.width)
+				&& (this.height <= is.height)
+				|| (this.width <= is.width)
+				&& (this.height == is.height));
+	}
+
+	/**
+	 * Returns if the size of this image is the same as the other image.
+	 * 
+	 * 
+	 * 
+	 * @param is
+	 * @return
+	 */
+	public boolean equals(ImageSize is) {
+		if ((this.width == 0)||(is.width == 0)) {
+			// width wildcard
+			return (this.height == is.height);
+		}
+		if ((this.height == 0)||(is.height == 0)) {
+			// height wildcard
+			return (this.width == is.width);
+		}
+		return ((this.width == is.width) && (this.height == is.height));
+	}
+
+	/**
+	 * @return
+	 */
+	public int getHeight() {
+		return height;
+	}
+
+	/**
+	 * @param height
+	 */
+	public void setHeight(int height) {
+		this.height = height;
+	}
+
+	/**
+	 * @return
+	 */
+	public int getWidth() {
+		return width;
+	}
+
+	/**
+	 * @param width
+	 */
+	public void setWidth(int width) {
+		this.width = width;
+	}
+
+	/**
+	 * Returns the aspect ratio.
+	 * 
+	 * Aspect ratio is (width/height). So it's <1 for portrait and  >1 for
+	 * landscape.
+	 * 
+	 * @return
+	 */
+	public float getAspect() {
+		return (height > 0) ? ((float) width / (float) height) : 0;
+	}
+	
+	/**
+	 * Returns a scaled copy of this image size. 
+	 * 
+	 * @param scale
+	 * @return
+	 */
+	public ImageSize getScaled(float scale) {
+		return new ImageSize((int) (width * scale), (int) (height * scale));
+	}
+	
+	/* (non-Javadoc)
+	 * @see java.lang.Object#toString()
+	 */
+	public String toString() {
+		String s = "[" + width + "x" + height + "]";
+		return s;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/util/XMLListLoader.java	Tue Mar 01 22:00:50 2011 +0100
@@ -0,0 +1,178 @@
+/* XMLListLoader -- Load an XML list into a Hashtable
+
+  Digital Image Library servlet components
+
+  Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
+
+  This program is free software; you can redistribute  it and/or modify it
+  under  the terms of  the GNU General  Public License as published by the
+  Free Software Foundation;  either version 2 of the  License, or (at your
+  option) any later version.
+   
+  Please read license.txt for the full details. A copy of the GPL
+  may be found at http://www.gnu.org/copyleft/lgpl.html
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+*/
+
+package digilib.util;
+
+// JAXP packages
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.apache.log4j.Logger;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/** Loads a simple XML list into a HashMap.
+ * 
+ * The XML file has an outer <code>list_tag</code>. Every entry is an 
+ * <code>entry_tag</code> with two attributes: the <code>key_att</code>
+ * key and the <code>value_att</code> value.
+ * 
+ * The file is read by the <code>loadURL</code> method, that returns a
+ * HashMap with the key-value pairs.
+ * 
+ * @author casties
+ */
+public class XMLListLoader {
+
+	private Logger logger = Logger.getLogger(this.getClass());
+	private String listTag = "list";
+	private String entryTag = "entry";
+	private String keyAtt = "key";
+	private String valueAtt = "value";
+
+	public XMLListLoader() {
+	}
+
+	public XMLListLoader(
+		String list_tag,
+		String entry_tag,
+		String key_att,
+		String value_att) {
+		logger.debug("xmlListLoader("+list_tag+","+entry_tag+","+key_att+","+value_att+")");
+		listTag = list_tag;
+		entryTag = entry_tag;
+		keyAtt = key_att;
+		valueAtt = value_att;
+	}
+
+	/**
+	 *  inner class XMLListParser to be called by the parser
+	 */
+	private class XMLListParser extends DefaultHandler {
+
+		private Map<String, String> listData;
+		private LinkedList<String> tagSpace;
+
+		public Map<String, String> getData() {
+			return listData;
+		}
+
+		// Parser calls this once at the beginning of a document
+		public void startDocument() throws SAXException {
+			listData = new HashMap<String, String>();
+			tagSpace = new LinkedList<String>();
+		}
+
+		// Parser calls this for each element in a document
+		public void startElement(
+			String namespaceURI,
+			String localName,
+			String qName,
+			Attributes atts)
+			throws SAXException {
+			//System.out.println("<"+qName);
+			// open a new namespace
+			tagSpace.addLast(qName);
+
+			// ist it an entry tag?
+			if (qName.equals(entryTag)) {
+				// is it inside a list tag?
+				if ((listTag.length() > 0) && (!tagSpace.contains(listTag))) {
+					logger.error("BOO: Entry "
+							+ entryTag
+							+ " not inside list "
+							+ listTag);
+					throw new SAXParseException(
+						"Entry " + entryTag + " not inside list " + listTag,
+						null);
+				}
+				// get the attributes
+				String key = atts.getValue(keyAtt);
+				String val = atts.getValue(valueAtt);
+				if ((key == null) || (val == null)) {
+					logger.error("BOO: Entry "
+							+ entryTag
+							+ " does not have Attributes "
+							+ keyAtt
+							+ ", "
+							+ valueAtt);
+					throw new SAXParseException(
+						"Entry "
+							+ entryTag
+							+ " does not have Attributes "
+							+ keyAtt
+							+ ", "
+							+ valueAtt,
+						null);
+				}
+				// add the values
+				//System.out.println("DATA: "+key+" = "+val);
+				listData.put(key, val);
+			}
+		}
+
+		public void endElement(
+			String namespaceURI,
+			String localName,
+			String qName)
+			throws SAXException {
+			// exit the namespace
+			tagSpace.removeLast();
+		}
+
+	}
+
+	/**
+	 *  load and parse a file (as URL)
+	 *    returns HashMap with list data
+	 */
+	public Map<String, String> loadURL(String path) throws SAXException, IOException {
+		//System.out.println("loadurl ("+path+")");
+		// Create a JAXP SAXParserFactory and configure it
+		SAXParserFactory spf = SAXParserFactory.newInstance();
+		spf.setNamespaceAware(true);
+
+		SAXParser parser = null;
+		try {
+			// Create a JAXP SAXParser
+			parser = spf.newSAXParser();
+
+		} catch (ParserConfigurationException e) {
+			throw new SAXException(e);
+		}
+
+		// create a list parser (keeps the data!)
+		XMLListParser listParser = new XMLListParser();
+
+		// Tell the SAXParser to parse the XML document
+		parser.parse(path, listParser);
+
+		return listParser.getData();
+	}
+
+}