changeset 339:6d2032b6121d gen2_1

new directory and cache work
author robcast
date Wed, 17 Nov 2004 18:17:34 +0100
parents 794a9f25f15c
children fe6bd1ff2376
files servlet/src/digilib/image/ImageLoaderDocuImage.java servlet/src/digilib/image/ImageOps.java servlet/src/digilib/image/JAIDocuImage.java servlet/src/digilib/image/JAIImageLoaderDocuImage.java servlet/src/digilib/io/AliasingDocuDirCache.java servlet/src/digilib/io/DigiDirectory.java servlet/src/digilib/io/DigiDirent.java servlet/src/digilib/io/DocuDirCache.java servlet/src/digilib/io/FileOps.java servlet/src/digilib/io/ImageFile.java servlet/src/digilib/io/ImageFileset.java servlet/src/digilib/io/SVGFile.java servlet/src/digilib/io/TextFile.java servlet/src/digilib/servlet/DigilibConfiguration.java servlet/src/digilib/servlet/DocumentBean.java servlet/src/digilib/servlet/Initialiser.java servlet/src/digilib/servlet/Scaler.java
diffstat 17 files changed, 4604 insertions(+), 943 deletions(-) [+]
line wrap: on
line diff
--- a/servlet/src/digilib/image/ImageLoaderDocuImage.java	Thu Jan 17 15:25:46 2002 +0100
+++ b/servlet/src/digilib/image/ImageLoaderDocuImage.java	Wed Nov 17 18:17:34 2004 +0100
@@ -1,164 +1,480 @@
 /* ImageLoaderDocuImage -- Image class implementation using JDK 1.4 ImageLoader
 
-  Digital Image Library servlet components
+ Digital Image Library servlet components
 
-  Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
+ Copyright (C) 2002, 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
+ 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
-
-*/
+ 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.image;
 
-import javax.servlet.*;
-import javax.servlet.http.*;
-import java.io.*;
-import java.util.*;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.AffineTransformOp;
+import java.awt.image.BufferedImage;
+import java.awt.image.ConvolveOp;
+import java.awt.image.Kernel;
+import java.awt.image.RescaleOp;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.Iterator;
 
-import java.awt.*;
-import java.awt.image.*;
-import java.awt.geom.*;
-import java.awt.image.renderable.*;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.FileImageInputStream;
+import javax.imageio.stream.ImageInputStream;
 
-import javax.imageio.*;
+import digilib.io.FileOpException;
+import digilib.io.ImageFile;
 
-import digilib.*;
-import digilib.io.*;
-
+/** Implementation of DocuImage using the ImageLoader API of Java 1.4 and Java2D. */
 public class ImageLoaderDocuImage extends DocuImageImpl {
 
-  private BufferedImage img;
+	/** image object */
+	protected BufferedImage img;
+
+	/** interpolation type */
+	protected RenderingHints renderHint;
+
+	/** ImageIO image reader */
+	protected ImageReader reader;
+
+	/** File that was read */
+	protected File imgFile;
+
+	/* loadSubimage is supported. */
+	public boolean isSubimageSupported() {
+		return true;
+	}
+
+	public void setQuality(int qual) {
+		quality = qual;
+		renderHint = new RenderingHints(null);
+		//hint.put(RenderingHints.KEY_ANTIALIASING,
+		// RenderingHints.VALUE_ANTIALIAS_OFF);
+		// setup interpolation quality
+		if (qual > 0) {
+			logger.debug("quality q1");
+			renderHint.put(RenderingHints.KEY_INTERPOLATION,
+					RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+		} else {
+			logger.debug("quality q0");
+			renderHint.put(RenderingHints.KEY_INTERPOLATION,
+					RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
+		}
+	}
 
-  public ImageLoaderDocuImage() {
-  }
+	public int getHeight() {
+		int h = 0;
+		try {
+			if (img == null) {
+				h = reader.getHeight(0);
+			} else {
+				h = img.getHeight();
+			}
+		} catch (IOException e) {
+			logger.debug("error in getHeight", e);
+		}
+		return h;
+	}
 
-  public ImageLoaderDocuImage(Utils u) {
-    util = u;
-  }
+	public int getWidth() {
+		int w = 0;
+		try {
+			if (img == null) {
+				w = reader.getWidth(0);
+			} else {
+				w = img.getWidth();
+			}
+		} catch (IOException e) {
+			logger.debug("error in getHeight", e);
+		}
+		return w;
+	}
+
+	/* load image file */
+	public void loadImage(ImageFile f) throws FileOpException {
+		logger.debug("loadImage " + f);
+		//System.gc();
+		try {
+			img = ImageIO.read(f);
+			if (img == null) {
+				throw new FileOpException("Unable to load File!");
+			}
+		} catch (IOException e) {
+			throw new FileOpException("Error reading image.");
+		}
+	}
 
-  /**
-   *  load image file
-   */
-  public void loadImage(File f) throws FileOpException {
-    util.dprintln(10, "loadImage!");
-    System.gc();
-    try {
-      for (int i = 0; i < ImageIO.getReaderFormatNames().length; i++) {
-    System.out.println("ImageLoader reader:"+ImageIO.getReaderFormatNames()[i]);
-      }
-      for (int i = 0; i < ImageIO.getWriterFormatNames().length; i++) {
-    System.out.println("ImageLoader writer:"+ImageIO.getWriterFormatNames()[i]);
-      }
-      img = ImageIO.read(f);
-      if (img == null) {
-        util.dprintln(3, "ERROR(loadImage): unable to load file");
-        throw new FileOpException("Unable to load File!");
-      }
-    }
-    catch (IOException e) {
-      throw new FileOpException("Error reading image.");
-    }
-  }
+	/**
+	 * Get an ImageReader for the image file.
+	 * 
+	 * @return
+	 */
+	public ImageReader getReader(ImageFile f) throws IOException {
+		logger.debug("preloadImage " + f);
+		if (reader != null) {
+			logger.debug("Reader was not null!");
+			// clean up old reader
+			dispose();
+		}
+		//System.gc();
+		RandomAccessFile rf = new RandomAccessFile(f, "r");
+		ImageInputStream istream = new FileImageInputStream(rf);
+		//Iterator readers = ImageIO.getImageReaders(istream);
+		String mt = f.getMimetype();
+		logger.debug("File type:" + mt);
+		Iterator readers = ImageIO.getImageReadersByMIMEType(mt);
+		if (!readers.hasNext()) {
+			throw new FileOpException("Unable to load File!");
+		}
+		reader = (ImageReader) 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;
+		return reader;
+	}
+
+	/* Load an image file into the Object. */
+	public void loadSubimage(ImageFile f, Rectangle region, int prescale)
+			throws FileOpException {
+		logger.debug("loadSubimage");
+		//System.gc();
+		try {
+			if ((reader == null) || (imgFile != f)) {
+				getReader(f);
+			}
+			// set up reader parameters
+			ImageReadParam readParam = reader.getDefaultReadParam();
+			readParam.setSourceRegion(region);
+			if (prescale > 1) {
+				readParam.setSourceSubsampling(prescale, prescale, 0, 0);
+			}
+			// read image
+			logger.debug("loading..");
+			img = reader.read(0, readParam);
+			logger.debug("loaded");
+		} catch (IOException e) {
+			throw new FileOpException("Unable to load File!");
+		}
+		if (img == null) {
+			throw new FileOpException("Unable to load File!");
+		}
+	}
+
+	/* write image of type mt to Stream */
+	public void writeImage(String mt, OutputStream ostream)
+			throws FileOpException {
+		logger.debug("writeImage");
+		try {
+			// setup output
+			String type = "png";
+			if (mt == "image/jpeg") {
+				type = "jpeg";
+			} else if (mt == "image/png") {
+				type = "png";
+			} else {
+				// unknown mime type
+				throw new FileOpException("Unknown mime type: " + mt);
+			}
+
+			/*
+			 * JPEG doesn't do transparency so we have to convert any RGBA image
+			 * to RGB :-( *Java2D BUG*
+			 */
+			if ((type == "jpeg") && (img.getColorModel().hasAlpha())) {
+				logger.debug("BARF: JPEG with transparency!!");
+				int w = img.getWidth();
+				int h = img.getHeight();
+				// BufferedImage.TYPE_INT_RGB seems to be fastest (JDK1.4.1,
+				// OSX)
+				int destType = BufferedImage.TYPE_INT_RGB;
+				BufferedImage img2 = new BufferedImage(w, h, destType);
+				img2.createGraphics().drawImage(img, null, 0, 0);
+				img = img2;
+			}
+
+			// render output
+			logger.debug("writing");
+			if (ImageIO.write(img, type, ostream)) {
+				// writing was OK
+				return;
+			} else {
+				throw new FileOpException(
+						"Error writing image: Unknown image format!");
+			}
+		} catch (IOException e) {
+			throw new FileOpException("Error writing image.");
+		}
+	}
 
-  /**
-   *  write image of type mt to Stream
-   */
-  public void writeImage(String mt, ServletResponse res)
-         throws FileOpException {
-    util.dprintln(10, "writeImage!");
-    try {
-      // setup output
-      String type = "png";
-      if (mt == "image/jpeg") {
-        type = "jpeg";
-      } else if (mt == "image/png") {
-        type = "png";
-      } else {
-        // unknown mime type
-        util.dprintln(2, "ERROR(writeImage): Unknown mime type "+mt);
-        throw new FileOpException("Unknown mime type: "+mt);
-      }
-      res.setContentType(mt);
-      // render output
-      if (ImageIO.write(img, type, res.getOutputStream())) {
-        // writing was OK
-        return;
-      } else {
-        throw new FileOpException("Error writing image: Unknown image format!");
-      }
-    } catch (IOException e) {
-      // e.printStackTrace();
-      throw new FileOpException("Error writing image.");
-    }
-  }
+	public void scale(double scale, double scaleY) throws ImageOpException {
+		logger.debug("scale");
+		/* for downscaling in high quality the image is blurred first */
+		if ((scale <= 0.5) && (quality > 1)) {
+			int bl = (int) Math.floor(1 / scale);
+			blur(bl);
+		}
+		/* then scaled */
+		AffineTransformOp scaleOp = new AffineTransformOp(AffineTransform
+				.getScaleInstance(scale, scale), renderHint);
+		BufferedImage scaledImg = null;
+		// enforce destination image type (*Java2D BUG*)
+		int type = img.getType();
+		// FIXME: which type would be best?
+		if ((quality > 0) && (type != 0)) {
+			logger.debug("creating destination image");
+			Rectangle2D dstBounds = scaleOp.getBounds2D(img);
+			scaledImg = new BufferedImage((int) dstBounds.getWidth(),
+					(int) dstBounds.getHeight(), type);
+		}
+		logger.debug("scaling...");
+		scaledImg = scaleOp.filter(img, scaledImg);
+		logger.debug("destination image type " + scaledImg.getType());
+		if (scaledImg == null) {
+			throw new ImageOpException("Unable to scale");
+		}
+		//DEBUG
+		logger.debug("SCALE: " + scale + " ->" + scaledImg.getWidth() + "x"
+				+ scaledImg.getHeight());
+		img = scaledImg;
+		scaledImg = null;
+	}
 
-  public int getWidth() {
-    if (img != null) {
-      return img.getWidth();
-    }
-    return 0;
-  }
+	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 = new Kernel(klen, klen, kern);
+		// blur with convolve operation
+		ConvolveOp blurOp = new ConvolveOp(blur, ConvolveOp.EDGE_NO_OP,
+				renderHint);
+		// blur needs explicit destination image type for color *Java2D BUG*
+		BufferedImage blurredImg = null;
+		if (img.getType() == BufferedImage.TYPE_3BYTE_BGR) {
+			blurredImg = new BufferedImage(img.getWidth(), img.getHeight(), img
+					.getType());
+		}
+		blurredImg = blurOp.filter(img, blurredImg);
+		if (blurredImg == null) {
+			throw new ImageOpException("Unable to scale");
+		}
+		img = blurredImg;
+	}
+
+	public void crop(int x_off, int y_off, int width, int height)
+			throws ImageOpException {
+		// setup Crop
+		BufferedImage croppedImg = img.getSubimage(x_off, y_off, width, height);
+		logger.debug("CROP:" + croppedImg.getWidth() + "x"
+				+ croppedImg.getHeight());
+		//DEBUG
+		//    util.dprintln(2, " time
+		// "+(System.currentTimeMillis()-startTime)+"ms");
+		if (croppedImg == null) {
+			throw new ImageOpException("Unable to crop");
+		}
+		img = croppedImg;
+	}
+
+	public void enhance(float mult, float add) throws ImageOpException {
+		/*
+		 * Only one constant should work regardless of the number of bands
+		 * according to the JDK spec. Doesn't work on JDK 1.4 for OSX and Linux
+		 * (at least). RescaleOp scaleOp = new RescaleOp( (float)mult,
+		 * (float)add, null); scaleOp.filter(img, img);
+		 */
+
+		/* The number of constants must match the number of bands in the image. */
+		int ncol = img.getColorModel().getNumComponents();
+		float[] dm = new float[ncol];
+		float[] da = new float[ncol];
+		for (int i = 0; i < ncol; i++) {
+			dm[i] = (float) mult;
+			da[i] = (float) add;
+		}
+		RescaleOp scaleOp = new RescaleOp(dm, da, null);
+		scaleOp.filter(img, img);
+	}
+
+	public void enhanceRGB(float[] rgbm, float[] rgba) throws ImageOpException {
+
+		/*
+		 * The number of constants must match the number of bands in the image.
+		 * We do only 3 (RGB) bands.
+		 */
+
+		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 ("
+							+ ncol + ")");
+			return;
+		}
+		RescaleOp scaleOp = new RescaleOp(rgbOrdered(rgbm), rgbOrdered(rgba),
+				null);
+		scaleOp.filter(img, img);
+	}
 
-  public int getHeight() {
-    if (img != null) {
-      return img.getHeight();
-    }
-    return 0;
-  }
-
-  /**
-   *  crop and scale image
-   *    take rectangle width,height at position x_off,y_off
-   *    and scale by scale
-   */
-   public void cropAndScale(int x_off, int y_off, int width, int height,
-         float scale, int qual) throws ImageOpException {
-    util.dprintln(10, "cropAndScale!");
-
-    int scaleInt = 0;
-    // setup interpolation quality
-    if (qual > 0) {
-      util.dprintln(4, "quality q1");
-      scaleInt = AffineTransformOp.TYPE_BILINEAR;
-    } else {
-      util.dprintln(4, "quality q0");
-      scaleInt = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
-    }
+	/**
+	 * Ensures that the array f is in the right order to map the images RGB
+	 * components. (not shure what happens
+	 */
+	public float[] rgbOrdered(float[] fa) {
+		/*
+		 * TODO: this is UGLY, UGLY!!
+		 */
+		float[] fb;
+		int t = img.getType();
+		if (img.getColorModel().hasAlpha()) {
+			fb = new float[4];
+			if ((t == BufferedImage.TYPE_INT_ARGB)
+					|| (t == BufferedImage.TYPE_INT_ARGB_PRE)) {
+				// RGB Type
+				fb[0] = fa[0];
+				fb[1] = fa[1];
+				fb[2] = fa[2];
+				fb[3] = 1f;
+			} else {
+				// this isn't tested :-(
+				fb[0] = 1f;
+				fb[1] = fa[0];
+				fb[2] = fa[1];
+				fb[3] = fa[2];
+			}
+		} else {
+			fb = new float[3];
+			if (t == BufferedImage.TYPE_3BYTE_BGR) {
+				// BGR Type (actually it looks like RBG...)
+				fb[0] = fa[0];
+				fb[1] = fa[2];
+				fb[2] = fa[1];
+			} else {
+				fb[0] = fa[0];
+				fb[1] = fa[1];
+				fb[2] = fa[2];
+			}
+		}
+		return fb;
+	}
 
-    // setup Crop
-    BufferedImage croppedImg = img.getSubimage(x_off, y_off, width, height);
-
-    img = null; // free img
-    util.dprintln(3, "CROP:"+croppedImg.getWidth()+"x"+croppedImg.getHeight()); //DEBUG
-//    util.dprintln(2, "  time "+(System.currentTimeMillis()-startTime)+"ms");
-
-    if (croppedImg == null) {
-      util.dprintln(2, "ERROR(cropAndScale): error in crop");
-      throw new ImageOpException("Unable to crop");
-    }
+	public void rotate(double angle) throws ImageOpException {
+		// setup rotation
+		double rangle = Math.toRadians(angle);
+		// create offset to make shure the rotated image has no negative
+		// coordinates
+		double w = img.getWidth();
+		double h = img.getHeight();
+		AffineTransform trafo = new AffineTransform();
+		// center of rotation
+		double x = (w / 2);
+		double y = (h / 2);
+		trafo.rotate(rangle, x, y);
+		// try rotation to see how far we're out of bounds
+		AffineTransformOp rotOp = new AffineTransformOp(trafo, renderHint);
+		Rectangle2D rotbounds = rotOp.getBounds2D(img);
+		double xoff = rotbounds.getX();
+		double yoff = rotbounds.getY();
+		// move image back in line
+		trafo
+				.preConcatenate(AffineTransform.getTranslateInstance(-xoff,
+						-yoff));
+		// transform image
+		rotOp = new AffineTransformOp(trafo, renderHint);
+		BufferedImage rotImg = rotOp.filter(img, null);
+		// calculate new bounding box
+		//Rectangle2D bounds = rotOp.getBounds2D(img);
+		if (rotImg == null) {
+			throw new ImageOpException("Unable to rotate");
+		}
+		img = rotImg;
+		// crop new image (with self-made rounding)
+		/*
+		 * img = rotImg.getSubimage( (int) (bounds.getX()+0.5), (int)
+		 * (bounds.getY()+0.5), (int) (bounds.getWidth()+0.5), (int)
+		 * (bounds.getHeight()+0.5));
+		 */
+	}
 
-    // setup scale
-    AffineTransformOp scaleOp = new AffineTransformOp(
-                      AffineTransform.getScaleInstance(scale, scale),
-                      scaleInt);
-    BufferedImage scaledImg = scaleOp.filter(croppedImg, null);
-    croppedImg = null; // free opCrop
+	public void mirror(double angle) throws ImageOpException {
+		// setup mirror
+		double mx = 1;
+		double my = 1;
+		double tx = 0;
+		double ty = 0;
+		if (Math.abs(angle - 0) < epsilon) { // 0 degree
+			mx = -1;
+			tx = getWidth();
+		} else if (Math.abs(angle - 90) < epsilon) { // 90 degree
+			my = -1;
+			ty = getHeight();
+		} else if (Math.abs(angle - 180) < epsilon) { // 180 degree
+			mx = -1;
+			tx = getWidth();
+		} else if (Math.abs(angle - 270) < epsilon) { // 270 degree
+			my = -1;
+			ty = getHeight();
+		} else if (Math.abs(angle - 360) < epsilon) { // 360 degree
+			mx = -1;
+			tx = getWidth();
+		}
+		AffineTransformOp mirOp = new AffineTransformOp(new AffineTransform(mx,
+				0, 0, my, tx, ty), renderHint);
+		BufferedImage mirImg = mirOp.filter(img, null);
+		if (mirImg == null) {
+			throw new ImageOpException("Unable to mirror");
+		}
+		img = mirImg;
+	}
 
-    if (scaledImg == null) {
-      util.dprintln(2, "ERROR(cropAndScale): error in scale");
-      throw new ImageOpException("Unable to scale");
-    }
-    img = scaledImg;
-  }
+	/*
+	 * (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;
+		}
+		img = null;
+	}
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/image/ImageOps.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,99 @@
+/* ImageOps -- convenience methods for images
+
+ Digital Image Library servlet components
+
+ Copyright (C) 2004 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 13.10.2004
+ */
+package digilib.image;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.Iterator;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.ImageInputStream;
+
+import org.apache.log4j.Logger;
+import org.marcoschmidt.image.ImageInfo;
+
+import digilib.io.FileOpException;
+import digilib.io.FileOps;
+import digilib.io.ImageFile;
+
+/**
+ * convenience methods for images
+ * 
+ * @author casties
+ */
+public class ImageOps {
+
+	private static Logger logger = Logger.getLogger(ImageOps.class);
+
+	/** Check and store image size and type of image in ImageFile */
+	public static boolean checkFile(ImageFile imgf) throws IOException {
+		// fileset to store the information
+		RandomAccessFile raf = new RandomAccessFile(imgf, "r");
+		// set up ImageInfo object
+		ImageInfo iif = new ImageInfo();
+		iif.setInput(raf);
+		iif.setCollectComments(false);
+		iif.setDetermineImageNumber(false);
+		logger.debug("identifying (ImageInfo) " + imgf);
+		// 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();
+			iif = null;
+		} else {
+			iif = null;
+			logger.debug("identifying (ImageIO) " + imgf);
+			/*
+			 * else use ImageReader
+			 */
+			ImageInputStream istream = ImageIO.createImageInputStream(raf);
+			Iterator readers = ImageIO.getImageReaders(istream);
+			if ((readers == null) || (!readers.hasNext())) {
+				throw new FileOpException("ERROR: unknown image file format!");
+			}
+			ImageReader reader = (ImageReader) 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);
+			ImageSize d = new ImageSize(reader.getWidth(0), reader.getHeight(0));
+			imgf.setSize(d);
+			//String t = reader.getFormatName();
+			String t = FileOps.mimeForFile(imgf);
+			imgf.setMimetype(t);
+			//logger.debug("  format:"+t);
+			// dispose the reader to free resources
+			reader.dispose();
+			raf.close();
+			reader = null;
+		}
+		logger.debug("image size: " + imgf.getSize());
+		return true;
+	}
+
+}
--- a/servlet/src/digilib/image/JAIDocuImage.java	Thu Jan 17 15:25:46 2002 +0100
+++ b/servlet/src/digilib/image/JAIDocuImage.java	Wed Nov 17 18:17:34 2004 +0100
@@ -2,7 +2,7 @@
 
   Digital Image Library servlet components
 
-  Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
+  Copyright (C) 2001, 2002, 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
@@ -20,149 +20,402 @@
 
 package digilib.image;
 
-import javax.servlet.*;
-import javax.servlet.http.*;
-import java.io.*;
-import java.util.*;
+import java.awt.RenderingHints;
+import java.awt.image.RenderedImage;
+import java.awt.image.renderable.ParameterBlock;
+import java.io.IOException;
+import java.io.OutputStream;
 
-import java.awt.*;
-import java.awt.image.*;
-import java.awt.image.renderable.*;
-import javax.media.jai.*;
+import javax.media.jai.BorderExtender;
+import javax.media.jai.Interpolation;
+import javax.media.jai.JAI;
+import javax.media.jai.KernelJAI;
+import javax.media.jai.ParameterBlockJAI;
+import javax.media.jai.operator.TransposeDescriptor;
+import javax.media.jai.operator.TransposeType;
 
-import digilib.*;
-import digilib.io.*;
+import digilib.io.ImageFile;
+import digilib.io.FileOpException;
 
-
+/** A DocuImage implementation using Java Advanced Imaging Library. */
 public class JAIDocuImage extends DocuImageImpl {
 
-  private RenderedImage img;
+	protected RenderedImage img;
+	protected Interpolation interpol = null;
+
+	/* Load an image file into the Object. */
+	public void loadImage(ImageFile f) throws FileOpException {
+		System.gc();
+		img = JAI.create("fileload", f.getAbsolutePath());
+		if (img == null) {
+			throw new FileOpException("Unable to load File!");
+		}
+	}
 
-  public JAIDocuImage() {
-  }
+	/* Write the current image to an OutputStream. */
+	public void writeImage(String mt, OutputStream ostream)
+		throws FileOpException {
+		try {
+			// setup output
+			ParameterBlock pb3 = new ParameterBlock();
+			pb3.addSource(img);
+			pb3.add(ostream);
+			if (mt == "image/jpeg") {
+				pb3.add("JPEG");
+			} else if (mt == "image/png") {
+				pb3.add("PNG");
+			} else {
+				// unknown mime type
+				throw new FileOpException("Unknown mime type: " + mt);
+			}
+			// render output
+			JAI.create("encode", pb3);
 
-  public JAIDocuImage(Utils u) {
-    util = u;
-  }
+		} catch (IOException e) {
+			throw new FileOpException("Error writing image.");
+		}
+	}
 
-  /**
-   *  load image file
-   */
-  public void loadImage(File f) throws FileOpException {
-    System.gc();
-    img = JAI.create("fileload", f.getAbsolutePath());
-    if (img == null) {
-      util.dprintln(3, "ERROR(loadImage): unable to load file");
-      throw new FileOpException("Unable to load File!");
-    }
-  }
+	/* Real setQuality implementation. 
+	 * Creates the correct Interpolation.
+	 */
+	public void setQuality(int qual) {
+		quality = qual;
+		// setup interpolation quality
+		if (qual > 1) {
+			logger.debug("quality q2");
+			interpol = Interpolation.getInstance(Interpolation.INTERP_BICUBIC);
+		} else if (qual == 1) {
+			logger.debug("quality q1");
+			interpol = Interpolation.getInstance(Interpolation.INTERP_BILINEAR);
+		} else {
+			logger.debug("quality q0");
+			interpol = Interpolation.getInstance(Interpolation.INTERP_NEAREST);
+		}
+	}
+
+	/** The width of the curent image in pixel.
+	 * @return Image width in pixels.
+	 */
+	public int getWidth() {
+		if (img != null) {
+			return img.getWidth();
+		}
+		return 0;
+	}
+
+	/** The height of the curent image in pixel.
+	 * @return Image height in pixels.
+	 */
+	public int getHeight() {
+		if (img != null) {
+			return img.getHeight();
+		}
+		return 0;
+	}
 
-  /**
-   *  write image of type mt to Stream
-   */
-  public void writeImage(String mt, ServletResponse res)
-         throws FileOpException {
-    try {
-    // setup output
-    ParameterBlock pb3 = new ParameterBlock();
-    pb3.addSource(img);
-    pb3.add(res.getOutputStream());
-    if (mt == "image/jpeg") {
-      pb3.add("JPEG");
-    } else if (mt == "image/png") {
-      pb3.add("PNG");
-    } else {
-      // unknown mime type
-      util.dprintln(2, "ERROR(writeImage): Unknown mime type "+mt);
-      throw new FileOpException("Unknown mime type: "+mt);
-    }
-    res.setContentType(mt);
-    // render output
-    JAI.create("encode", pb3);
+	/* scales the current image */
+	public void scale(double scale, double scaleY) throws ImageOpException {
+		logger.debug("scale");
+		if ((scale < 1)
+			&& (img.getColorModel().getPixelSize() == 1)
+			&& (quality > 0)) {
+			/*
+			 * "SubsampleBinaryToGray" for downscaling BW
+			 */
+			scaleBinary((float) scale);
+		} else if ((scale <= 0.5) && (quality > 1)) {
+			/*
+			 * blur and "Scale" for downscaling color images
+			 */
+			int subsample = (int) Math.floor(1 / scale);
+			blur(subsample);
+			scaleAll((float) scale);
+		} else {
+			/*
+			 * "Scale" for the rest
+			 */
+			scaleAll((float) scale);
+		}
+
+		//DEBUG
+		logger.debug("SCALE: " + scale + " ->" + img.getWidth() + "x" + img.getHeight());
+
+	}
+
+	public void scaleAll(float scale) throws ImageOpException {
+		RenderedImage scaledImg;
+		//DEBUG
+		logger.debug("scaleAll: " + scale);
+		ParameterBlockJAI param = new ParameterBlockJAI("Scale");
+		param.addSource(img);
+		param.setParameter("xScale", scale);
+		param.setParameter("yScale", scale);
+		param.setParameter("interpolation", interpol);
+		// hint with border extender
+		RenderingHints hint =
+			new RenderingHints(
+				JAI.KEY_BORDER_EXTENDER,
+				BorderExtender.createInstance(BorderExtender.BORDER_COPY));
+		// scale
+		scaledImg = JAI.create("Scale", param, hint);
+
+		if (scaledImg == null) {
+			throw new ImageOpException("Unable to scale");
+		}
+		img = scaledImg;
+	}
 
-    } catch (IOException e) {
-      throw new FileOpException("Error writing image.");
-    }
-  }
+	public void blur(int radius) throws ImageOpException {
+		RenderedImage blurredImg;
+		//DEBUG
+		logger.debug("blur: " + radius);
+		int klen = Math.max(radius, 2);
+		int ksize = klen * klen;
+		float f = 1f / ksize;
+		float[] kern = new float[ksize];
+		for (int i = 0; i < ksize; i++) {
+			kern[i] = f;
+		}
+		KernelJAI blur = new KernelJAI(klen, klen, kern);
+		ParameterBlockJAI param = new ParameterBlockJAI("Convolve");
+		param.addSource(img);
+		param.setParameter("kernel", blur);
+		// hint with border extender
+		RenderingHints hint =
+			new RenderingHints(
+				JAI.KEY_BORDER_EXTENDER,
+				BorderExtender.createInstance(BorderExtender.BORDER_COPY));
+		blurredImg = JAI.create("Convolve", param, hint);
+		if (blurredImg == null) {
+			throw new ImageOpException("Unable to scale");
+		}
+		img = blurredImg;
+	}
 
-  public int getWidth() {
-    if (img != null) {
-      return img.getWidth();
-    }
-    return 0;
-  }
+	public void scaleBinary(float scale) throws ImageOpException {
+		RenderedImage scaledImg;
+		//DEBUG
+		logger.debug("scaleBinary: " + scale);
+		ParameterBlockJAI param =
+			new ParameterBlockJAI("SubsampleBinaryToGray");
+		param.addSource(img);
+		param.setParameter("xScale", scale);
+		param.setParameter("yScale", scale);
+		// hint with border extender
+		RenderingHints hint =
+			new RenderingHints(
+				JAI.KEY_BORDER_EXTENDER,
+				BorderExtender.createInstance(BorderExtender.BORDER_COPY));
+		// scale
+		scaledImg = JAI.create("SubsampleBinaryToGray", param, hint);
+		if (scaledImg == null) {
+			throw new ImageOpException("Unable to scale");
+		}
+		img = scaledImg;
+	}
 
-  public int getHeight() {
-    if (img != null) {
-      return img.getHeight();
-    }
-    return 0;
-  }
-
+	/* crops the current image */
+	public void crop(int x_off, int y_off, int width, int height)
+		throws ImageOpException {
+		// setup Crop
+		ParameterBlock param = new ParameterBlock();
+		param.addSource(img);
+		param.add((float) x_off);
+		param.add((float) y_off);
+		param.add((float) width);
+		param.add((float) height);
+		RenderedImage croppedImg = JAI.create("crop", param);
 
-  /**
-   *  crop and scale image
-   *    take rectangle width,height at position x_off,y_off
-   *    and scale by scale
-   */
-   public void cropAndScale(int x_off, int y_off, int width, int height,
-         float scale, int qual) throws ImageOpException {
+		logger.debug("CROP: "
+				+ x_off
+				+ ","
+				+ y_off
+				+ ", "
+				+ width
+				+ ","
+				+ height
+				+ " ->"
+				+ croppedImg.getWidth()
+				+ "x"
+				+ croppedImg.getHeight());
+		//DEBUG
+
+		if (croppedImg == null) {
+			throw new ImageOpException("Unable to crop");
+		}
+		img = croppedImg;
+	}
+
+	/* rotates the current image */
+	public void rotate(double angle) throws ImageOpException {
+		RenderedImage rotImg;
+		// convert degrees to radians
+		double rangle = Math.toRadians(angle);
+		double x = img.getWidth() / 2;
+		double y = img.getHeight() / 2;
 
-    Interpolation scaleInt = null;
-    // setup interpolation quality
-    if (qual > 1) {
-      util.dprintln(4, "quality q2");
-      scaleInt = Interpolation.getInstance(Interpolation.INTERP_BICUBIC);
-    } else if (qual == 1) {
-      util.dprintln(4, "quality q1");
-      scaleInt = Interpolation.getInstance(Interpolation.INTERP_BILINEAR);
-    } else {
-      util.dprintln(4, "quality q0");
-      scaleInt = Interpolation.getInstance(Interpolation.INTERP_NEAREST);
-    }
+		// optimize rotation by right angles
+		TransposeType rotOp = null;
+		if (Math.abs(angle - 0) < epsilon) {
+			// 0 degree
+			return;
+		} else if (Math.abs(angle - 90) < epsilon) {
+			// 90 degree
+			rotOp = TransposeDescriptor.ROTATE_90;
+		} else if (Math.abs(angle - 180) < epsilon) {
+			// 180 degree
+			rotOp = TransposeDescriptor.ROTATE_180;
+		} else if (Math.abs(angle - 270) < epsilon) {
+			// 270 degree
+			rotOp = TransposeDescriptor.ROTATE_270;
+		} else if (Math.abs(angle - 360) < epsilon) {
+			// 360 degree
+			return;
+		}
+		if (rotOp != null) {
+			// use Transpose operation
+			ParameterBlock pb = new ParameterBlock();
+			pb.addSource(img);
+			pb.add(rotOp);
+			rotImg = JAI.create("transpose", pb);
+		} else {
+			// setup "normal" rotation
+			ParameterBlock param = new ParameterBlock();
+			param.addSource(img);
+			param.add((float) x);
+			param.add((float) y);
+			param.add((float) rangle);
+			param.add(interpol);
 
-    // setup Crop
-    ParameterBlock pb1 = new ParameterBlock();
-    pb1.addSource(img);
-    pb1.add((float)x_off);
-    pb1.add((float)y_off);
-    pb1.add((float)width);
-    pb1.add((float)height);
-    RenderedImage croppedImg = JAI.create("crop", pb1);
-    img = null; // free img
+			rotImg = JAI.create("rotate", param);
+		}
+
+		logger.debug("ROTATE: "
+				+ x
+				+ ","
+				+ y
+				+ ", "
+				+ angle
+				+ " ("
+				+ rangle
+				+ ")"
+				+ " ->"
+				+ rotImg.getWidth()
+				+ "x"
+				+ rotImg.getHeight());
+		//DEBUG
+
+		if (rotImg == null) {
+			throw new ImageOpException("Unable to rotate");
+		}
+		img = rotImg;
+	}
 
-    util.dprintln(3, "CROP:"+croppedImg.getWidth()+"x"+croppedImg.getHeight()); //DEBUG
+	/* mirrors the current image
+	 * works only horizontal and vertical
+	 */
+	public void mirror(double angle) throws ImageOpException {
+		RenderedImage mirImg;
+		// only mirroring by right angles
+		TransposeType rotOp = null;
+		if (Math.abs(angle) < epsilon) {
+			// 0 degree
+			rotOp = TransposeDescriptor.FLIP_HORIZONTAL;
+		} else if (Math.abs(angle - 90) < epsilon) {
+			// 90 degree
+			rotOp = TransposeDescriptor.FLIP_VERTICAL;
+		} else if (Math.abs(angle - 180) < epsilon) {
+			// 180 degree
+			rotOp = TransposeDescriptor.FLIP_HORIZONTAL;
+		} else if (Math.abs(angle - 270) < epsilon) {
+			// 270 degree
+			rotOp = TransposeDescriptor.FLIP_VERTICAL;
+		} else if (Math.abs(angle - 360) < epsilon) {
+			// 360 degree
+			rotOp = TransposeDescriptor.FLIP_HORIZONTAL;
+		}
+		// use Transpose operation
+		ParameterBlock param = new ParameterBlock();
+		param.addSource(img);
+		param.add(rotOp);
+		mirImg = JAI.create("transpose", param);
 
-    if (croppedImg == null) {
-      util.dprintln(2, "ERROR(cropAndScale): error in crop");
-      throw new ImageOpException("Unable to crop");
-    }
+		if (mirImg == null) {
+			throw new ImageOpException("Unable to flip");
+		}
+		img = mirImg;
+	}
+
+	/* contrast and brightness enhancement */
+	public void enhance(float mult, float add) throws ImageOpException {
+		RenderedImage enhImg;
+		double[] ma = { mult };
+		double[] aa = { add };
+		// use Rescale operation
+		ParameterBlock param = new ParameterBlock();
+		param.addSource(img);
+		param.add(ma);
+		param.add(aa);
+		enhImg = JAI.create("rescale", param);
 
-    // setup scale
-    ParameterBlock pb2 = new ParameterBlock();
-    pb2.addSource(croppedImg);
-    pb2.add(scale);
-    pb2.add(scale);
-    pb2.add(0f);
-    pb2.add(0f);
-    pb2.add(scaleInt);
-    // the following is nice but way too slow...
-    //if (opCrop.getColorModel().getPixelSize() < 8) {
-    // change color model if necessary
-    //  util.dprintln("converting color model...");
-    //  BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY);
-    //  ImageLayout lay = new ImageLayout(bi);
-    //  rh = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, lay);
-    //}
-    RenderedImage scaledImg = JAI.create("scale", pb2);
-    croppedImg = null; // free opCrop
+		logger.debug("ENHANCE: *"
+				+ mult
+				+ ", +"
+				+ add
+				+ " ->"
+				+ enhImg.getWidth()
+				+ "x"
+				+ enhImg.getHeight());
+		//DEBUG
+
+		if (enhImg == null) {
+			throw new ImageOpException("Unable to enhance");
+		}
+		img = enhImg;
+	}
 
-    if (scaledImg == null) {
-      util.dprintln(2, "ERROR(cropAndScale): error in scale");
-      throw new ImageOpException("Unable to scale");
-    }
+	/* (non-Javadoc)
+	 * @see digilib.image.DocuImage#enhanceRGB(float[], float[])
+	 */
+	public void enhanceRGB(float[] rgbm, float[] rgba)
+		throws ImageOpException {
+		RenderedImage enhImg;
+		int nb = rgbm.length;
+		double[] ma = new double[nb];
+		double[] aa = new double[nb];
+		for (int i = 0; i < nb; i++) {
+			ma[i] = rgbm[i];
+			aa[i] = rgba[i];
+		}
+		// use Rescale operation
+		ParameterBlock param = new ParameterBlock();
+		param.addSource(img);
+		param.add(ma);
+		param.add(aa);
+		enhImg = JAI.create("rescale", param);
 
-    img = scaledImg;
-  }
+		logger.debug("ENHANCE_RGB: *"
+				+ rgbm
+				+ ", +"
+				+ rgba
+				+ " ->"
+				+ enhImg.getWidth()
+				+ "x"
+				+ enhImg.getHeight());
+		//DEBUG
+
+		if (enhImg == null) {
+			throw new ImageOpException("Unable to enhanceRGB");
+		}
+		img = enhImg;
+	}
+
+	/* (non-Javadoc)
+	 * @see digilib.image.DocuImage#dispose()
+	 */
+	public void dispose() {
+		img = null;
+	}
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/image/JAIImageLoaderDocuImage.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,184 @@
+/* JAIImageLoaderDocuImage -- Image class implementation using JAI's ImageLoader Plugin
+
+  Digital Image Library servlet components
+
+  Copyright (C) 2002, 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.image;
+
+import java.awt.Rectangle;
+import java.awt.image.renderable.ParameterBlock;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.Iterator;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.FileImageInputStream;
+import javax.imageio.stream.ImageInputStream;
+import javax.media.jai.JAI;
+
+import digilib.io.FileOpException;
+import digilib.io.ImageFile;
+
+/** DocuImage implementation using the Java Advanced Imaging API and the ImageLoader
+ * API of Java 1.4.
+ */
+public class JAIImageLoaderDocuImage extends JAIDocuImage {
+
+	/** ImageIO image reader */
+	protected ImageReader reader;
+	/** current image file */
+	protected File imgFile;
+
+	/* loadSubimage is supported. */
+	public boolean isSubimageSupported() {
+		return true;
+	}
+
+	public int getHeight() {
+		int h = 0;
+		try {
+			if (img == null) {
+				h = reader.getHeight(0);
+			} else {
+				h = img.getHeight();
+			}
+		} catch (IOException e) {
+			logger.debug("error in getHeight", e);
+		}
+		return h;
+	}
+
+	public int getWidth() {
+		int w = 0;
+		try {
+			if (img == null) {
+				w = reader.getWidth(0);
+			} else {
+				w = img.getWidth();
+			}
+		} catch (IOException e) {
+			logger.debug("error in getHeight", e);
+		}
+		return w;
+	}
+
+	/* Load an image file into the Object. */
+	public void loadImage(ImageFile f) throws FileOpException {
+		logger.debug("loadImage: "+f);
+		//System.gc();
+		img = JAI.create("ImageRead", f.getAbsolutePath());
+		if (img == null) {
+			throw new FileOpException("Unable to load File!");
+		}
+	}
+
+	/* Get an ImageReader for the image file. */
+	public ImageReader getReader(ImageFile f) throws IOException {
+		logger.debug("preloadImage: "+f);
+		//System.gc();
+		RandomAccessFile rf = new RandomAccessFile(f, "r");
+		ImageInputStream istream = new FileImageInputStream(rf);
+		//Iterator readers = ImageIO.getImageReaders(istream);
+		Iterator readers = ImageIO.getImageReadersByMIMEType(f.getMimetype());
+		if (! readers.hasNext()) {
+			throw new FileOpException("Unable to load File!");
+		}
+		reader = (ImageReader) readers.next();
+		logger.debug("JAIImageIO: this reader: " + reader.getClass());
+		while (readers.hasNext()) {
+			logger.debug("  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)
+		throws FileOpException {
+		logger.debug("loadSubimage: "+f);
+		//System.gc();
+		try {
+			if ((reader == null) || (imgFile != f)) {
+				getReader(f);
+			}
+			ImageReadParam readParam = reader.getDefaultReadParam();
+			readParam.setSourceRegion(region);
+			readParam.setSourceSubsampling(prescale, prescale, 0, 0);
+			img = reader.read(0, readParam);
+			/* JAI imageread seems to ignore the readParam :-(
+			ImageInputStream istream = (ImageInputStream) reader.getInput();
+			ParameterBlockJAI pb = new ParameterBlockJAI("imageread");
+			pb.setParameter("Input", istream);
+			pb.setParameter("ReadParam", readParam);
+			pb.setParameter("Reader", reader);
+			img = JAI.create("imageread", pb);
+			*/
+		} catch (IOException e) {
+			throw new FileOpException("Unable to load File!");
+		}
+		if (img == null) {
+			throw new FileOpException("Unable to load File!");
+		}
+		imgFile = f;
+	}
+
+
+	/* Write the current image to an OutputStream. */
+	public void writeImage(String mt, OutputStream ostream)
+		throws FileOpException {
+		logger.debug("writeImage");
+		try {
+			// setup output
+			ParameterBlock pb3 = new ParameterBlock();
+			pb3.addSource(img);
+			pb3.add(ostream);
+			if (mt == "image/jpeg") {
+				pb3.add("JPEG");
+			} else if (mt == "image/png") {
+				pb3.add("PNG");
+			} else {
+				// unknown mime type
+				throw new FileOpException("Unknown mime type: " + mt);
+			}
+			// render output
+			JAI.create("ImageWrite", pb3);
+		} catch (IOException e) {
+			throw new FileOpException("Error writing image.");
+		}
+	}
+
+	/* (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!
+		reader.dispose();
+		reader = null;
+		img = null;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/AliasingDocuDirCache.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,91 @@
+/*
+ * AliasingDocuDirCache -- DocuDirCache using alias entries from config file
+ * 
+ * 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 04.11.2003
+ */
+
+package digilib.io;
+
+import java.io.File;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * @author casties
+ *  
+ */
+public class AliasingDocuDirCache extends DocuDirCache {
+
+	/**
+	 * @param baseDirs
+	 * @param fileClasses
+	 * @param confFileName
+	 * @throws FileOpException
+	 */
+	public AliasingDocuDirCache(File confFile) throws FileOpException {
+		// create standard DocuDirCache
+		super();
+		Map pathMap = null;
+		// read alias config file
+		try {
+			// load into pathMap
+			XMLListLoader mapLoader = new XMLListLoader("digilib-aliases",
+					"mapping", "link", "dir");
+			pathMap = mapLoader.loadURL(confFile.toURL().toString());
+		} catch (Exception e) {
+			throw new FileOpException("ERROR loading mapping file: " + e);
+		}
+		if (pathMap == null) {
+			throw new FileOpException("ERROR: unable to load mapping file!");
+		}
+
+		/*
+		 * load map entries into cache
+		 */
+
+		for (Iterator i = pathMap.keySet().iterator(); i.hasNext();) {
+			String link = FileOps.normalName((String) i.next());
+			String dn = (String) pathMap.get(link);
+			File dir = FileOps.getRealFile(dn);
+			if (dir.isDirectory()) {
+				logger.debug("Aliasing dir: " + link);
+				DigiDirectory destDir = new DigiDirectory(dir, dn, null);
+				// add the alias name
+				putName(link, destDir);
+				// add the real dir
+				putDir(destDir);
+			}
+		}
+	}
+
+	/**
+	 * Adds a DocuDirectory under another name to the cache.
+	 * 
+	 * @param name
+	 * @param newdir
+	 */
+	public void putName(String name, DigiDirectory newdir) {
+		Object oldkey = map.put(name, newdir);
+		if (oldkey != null) {
+			logger
+					.warn("Duplicate key in AliasingDocuDirCache.put -- replaced!");
+		}
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/DigiDirectory.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,380 @@
+/* DigiDirectory.java -- digilib directory object
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 2004 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 03.11.2004
+ */
+package digilib.io;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.xml.sax.SAXException;
+
+/**
+ * digilib directory object
+ * 
+ * @author casties
+ *  
+ */
+public class DigiDirectory extends DigiDirent {
+
+	protected static boolean isFile = false;
+
+	protected static boolean isDirectory = true;
+
+	protected DigiDirent[] entries;
+
+	protected int[][] indexes;
+
+	protected File dir;
+
+	/** directory name (digilib canonical form) */
+	protected String dlPath = null;
+
+	protected long mtime = 0;
+
+	protected boolean isDirRead = false;
+
+	protected Map unresolvedMeta;
+
+	/**
+	 * @param parent
+	 * @param dir
+	 */
+	public DigiDirectory(String dlPath) {
+		super(FileOps.dlName(dlPath), null);
+	}
+
+	/**
+	 * @param dir
+	 * @param parent
+	 */
+	public DigiDirectory(File dir, DigiDirectory parent) {
+		super(dir.getName(), parent);
+		this.dir = dir;
+		this.dlPath = parent.getDLPath() + "/" + dir.getName();
+	}
+
+	/**
+	 * @param dir
+	 * @param dlpath
+	 * @param parent
+	 */
+	public DigiDirectory(File dir, String dlpath, DigiDirectory parent) {
+		super(dir.getName(), parent);
+		this.dlPath = dlpath;
+		this.dir = dir;
+	}
+
+	public boolean exists() {
+		return ((dir != null) && (dir.isDirectory()));
+	}
+
+	/**
+	 * Returns the file of the class at the index.
+	 * 
+	 * @param index
+	 * @param fc
+	 *            fileClass
+	 * @return
+	 */
+	public DigiDirent get(int index, int fc) {
+		if (!isDirRead) {
+			// read directory now
+			if (readDir() < 1) {
+				return null;
+			}
+		}
+		try {
+			return (DigiDirent) entries[indexes[fc][index]];
+		} catch (Exception e) {
+		}
+		return null;
+	}
+
+	/**
+	 * Returns the file with the name <code>fn</code>.
+	 * 
+	 * @param fn
+	 *            filename
+	 * @return
+	 */
+	public DigiDirent get(String fn) {
+		if (!isDirRead) {
+			// read directory now
+			if (readDir() < 1) {
+				return null;
+			}
+		}
+		// search for exact match
+		int idx = Arrays.binarySearch(entries, fn);
+		if (idx >= 0) {
+			return entries[idx];
+		} else {
+			// try closest matches without extension
+			idx = -idx - 1;
+			int imax = entries.length;
+			String fb = FileOps.basename(fn);
+			if ((idx < imax)
+					&& (FileOps.basename(entries[idx].getName()).equals(fb))) {
+				// idx matches
+				return entries[idx];
+			} else if ((idx > 0)
+					&& (FileOps.basename(entries[idx - 1].getName()).equals(fb))) {
+				// idx-1 matches
+				return entries[idx - 1];
+			} else if ((idx + 1 < imax)
+					&& (FileOps.basename(entries[idx + 1].getName()).equals(fb))) {
+				// idx+1 matches
+				return entries[idx + 1];
+			}
+		}
+		return null;
+	}
+
+	
+	/**
+	 * Reads the names of the files in the directory. Fills the filenames array.
+	 * Returns the number of files.
+	 * 
+	 * @return
+	 */
+	public int readDir() {
+		if (!exists()) {
+			return -1;
+		}
+		File[] allFiles = null;
+		// list all files in the directory
+		allFiles = dir.listFiles();
+		if (allFiles == null) {
+			// not a directory
+			isDirRead = true;
+			return -1;
+		}
+		Arrays.sort(allFiles);
+		int nfiles = allFiles.length;
+		entries = new DigiDirent[nfiles];
+		// create new index lists for all file classes
+		int nfc = FileOps.NUM_CLASSES;
+		indexes = new int[nfc][nfiles];
+		// index pointers for the file classes
+		int[] lastidx = new int[nfc];
+		Map hints = FileOps.newHints();
+		// go through all files
+		for (int dirIdx = 0; dirIdx < nfiles; dirIdx++) {
+			File f = allFiles[dirIdx];
+			String fn = f.getName();
+			int fc = FileOps.classForFilename(fn);
+			// create the right kind of Dirent
+			DigiDirent df = FileOps.fileForClass(fc, f, this, hints);
+			// add the file to our list
+			entries[dirIdx] = df;
+			// add to the indexes
+			if (fc >= 0) {
+				indexes[fc][lastidx[fc]++] = dirIdx;
+			}
+		}
+		// copy out the index arrays
+		for (int i = 0; i < nfc; i++) {
+			int[] idxs = new int[lastidx[i]];
+			System.arraycopy(indexes[i], 0, idxs, 0, lastidx[i]);
+			indexes[i] = idxs;
+		}
+		// update modification time
+		mtime = dir.lastModified();
+		// read metadata as well
+		readMeta();
+		isDirRead = true;
+		return nfiles;
+	}
+
+	/**
+	 * Check to see if the directory has been modified and reread if necessary.
+	 * 
+	 * @return boolean the directory is valid
+	 */
+	public void check() {
+		if (isDirRead && (dir.lastModified() > mtime)) {
+			// on-disk modification time is more recent
+			readDir();
+		} else if (!isDirRead) {
+			readDir();
+		}
+	}
+
+	/**
+	 * Read directory metadata.
+	 *  
+	 */
+	public void readMeta() {
+		// check for directory metadata...
+		File mf = new File(dir, "index.meta");
+		if (mf.canRead()) {
+			XMLMetaLoader ml = new XMLMetaLoader();
+			try {
+				// read directory meta file
+				Map fileMeta = ml.loadURL(mf.getAbsolutePath());
+				if (fileMeta == null) {
+					throw new IOException("XMLMetaloader returned no data!");
+				}
+				// meta for the directory itself is in the "" bin
+				meta = (Map) fileMeta.remove("");
+				// read meta for files in this directory
+				storeFileMeta(fileMeta, null);
+				// is there meta for other files left?
+				if (fileMeta.size() > 0) {
+					unresolvedMeta = fileMeta;
+				}
+			} catch (SAXException e) {
+				logger.warn("error parsing index.meta", e);
+			} catch (IOException e) {
+				logger.warn("error reading index.meta", e);
+			}
+		}
+		readParentMeta();
+		isMetaRead = true;
+	}
+
+	/**
+	 * Read metadata from all known parent directories.
+	 *  
+	 */
+	public void readParentMeta() {
+		// check the parent directories for additional file meta
+		DigiDirectory dd = getParent();
+		String path = dir.getName();
+		while (dd != null) {
+			if (dd.hasUnresolvedMeta()) {
+				storeFileMeta(dd.getUnresolvedMeta(), path);
+			}
+			// prepend parent dir path
+			path = dd.getDir().getName() + "/" + path;
+			// become next parent
+			dd = dd.getParent();
+		}
+	}
+
+	/**
+	 * Stores metadata in the files in this directory.
+	 * 
+	 * Takes a Map with meta-information, adding the relative path before the
+	 * lookup.
+	 * 
+	 * @param fileMeta
+	 * @param relPath
+	 * @param fc
+	 *            fileClass
+	 */
+	protected void storeFileMeta(Map fileMeta, String relPath) {
+		if (entries == null) {
+			// there are no files
+			return;
+		}
+		String path = (relPath != null) ? (relPath + "/") : "";
+		// iterate through the list of files in this directory
+		for (int i = 0; i < entries.length; i++) {
+			DigiDirent f = entries[i];
+			// prepend path to the filename
+			String fn = path + f.getName();
+			// look up meta for this file
+			if (relPath == null) {
+				// remove from map the same directory
+				f.addMeta((Map) fileMeta.remove(fn));
+			} else {
+				// copy from map in other directories
+				f.addMeta((Map) fileMeta.get(fn));
+			}
+		}
+	}
+
+	/*
+	 * boring getters and setters
+	 */
+
+	public boolean hasUnresolvedMeta() {
+		return ((unresolvedMeta != null) && unresolvedMeta.isEmpty());
+	}
+
+	/**
+	 * @return Returns the unresolvedMeta.
+	 */
+	public Map getUnresolvedMeta() {
+		return unresolvedMeta;
+	}
+
+	/**
+	 * @return Returns the dir.
+	 */
+	public File getDir() {
+		return dir;
+	}
+
+	/**
+	 * @param dir
+	 *            The dir to set.
+	 */
+	public void setDir(File dir) {
+		this.dir = dir;
+	}
+
+	/**
+	 * @return Returns the dlPath.
+	 */
+	public String getDLPath() {
+		return dlPath;
+	}
+
+	/**
+	 * @param dlPath
+	 *            The dlPath to set.
+	 */
+	public void setDlPath(String dlPath) {
+		this.dlPath = dlPath;
+	}
+
+	/**
+	 * The number of files in this directory.
+	 * 
+	 * @return
+	 */
+	public int getSize() {
+		return (entries != null) ? entries.length : 0;
+	}
+
+	/**
+	 * The number of files of a file class in this directory.
+	 * 
+	 * @return
+	 */
+	public int getSize(int fc) {
+		try {
+			return indexes[fc].length;
+		} catch (Exception e) {
+		}
+		return 0;
+	}
+
+	/**
+	 * @return Returns the mtime.
+	 */
+	public long getMtime() {
+		return mtime;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/DigiDirent.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,191 @@
+/* DigiDirent.java -- an entry in a directory
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 2004 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 03.11.2004
+ */
+
+package digilib.io;
+
+import java.io.File;
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+
+/**
+ * an entry in a directory
+ * 
+ * @author casties
+ *  
+ */
+public class DigiDirent implements Comparable {
+
+	protected static Logger logger = Logger.getLogger(DigiDirent.class);
+	
+	protected static int fileClass = FileOps.CLASS_NONE;
+	
+	protected static boolean isFile = true;
+
+	protected static boolean isDirectory = false;
+
+	protected String name;
+
+	protected DigiDirectory parent;
+
+	protected Map meta;
+
+	protected boolean isMetaRead = false;
+
+	/**
+	 *  
+	 */
+	public DigiDirent() {
+		super();
+	}
+
+	/**
+	 * @param name
+	 * @param parent
+	 */
+	public DigiDirent(String name) {
+		super();
+		this.name = name;
+	}
+
+	/**
+	 * @param name
+	 * @param parent
+	 */
+	public DigiDirent(String name, DigiDirectory parent) {
+		super();
+		this.name = name;
+		this.parent = parent;
+	}
+
+	public boolean isFile() {
+		return isFile;
+	}
+
+	public boolean isDirectory() {
+		return isDirectory;
+	}
+
+	/**
+	 * @return Returns the file class.
+	 */
+	public int getFileClass() {
+		return fileClass;
+	}
+	
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * @return Returns the parent.
+	 */
+	public DigiDirectory getParent() {
+		return parent;
+	}
+
+	/**
+	 * @param parent
+	 *            The parent to set.
+	 */
+	public void setParent(DigiDirectory parent) {
+		this.parent = parent;
+	}
+
+	/**
+	 * @return Returns the meta.
+	 */
+	public Map getMeta() {
+		return meta;
+	}
+
+	/**
+	 * @param meta
+	 *            The meta to set.
+	 */
+	public void setMeta(Map meta) {
+		this.meta = meta;
+	}
+
+	/**
+	 * Adds metadata to this files metadata.
+	 * 
+	 * @param meta
+	 */
+	public void addMeta(Map meta) {
+		if (meta == null) {
+			return;
+		}
+		if ((this.meta == null) || (this.meta.isEmpty())) {
+			this.meta = meta;
+		} else {
+			this.meta.putAll(meta);
+		}
+	}
+
+	/**
+	 * Reads meta-data for this file if there is any.
+	 *  
+	 */
+	public void readMeta() {
+		if (isMetaRead || (parent == null) || (!parent.exists())) {
+			// there is already metadata or there is no file
+			return;
+		}
+		// metadata is in the file {filename}.meta
+		String fn = parent.getDir().getAbsolutePath();
+		File mf = new File(fn, name + ".meta");
+		if (mf.canRead()) {
+			XMLMetaLoader ml = new XMLMetaLoader();
+			try {
+				// read meta file
+				Map fileMeta = ml.loadURL(mf.getAbsolutePath());
+				// meta for this file is in an entry with its name
+				addMeta((Map)fileMeta.get(name));
+			} catch (Exception e) {
+				Logger.getLogger(this.getClass()).warn(
+						"error reading file .meta", e);
+			}
+		}
+		isMetaRead = true;
+	}
+	
+	/**
+	 * Checks metadata.
+	 *  
+	 */
+	public void check() {
+		if (isMetaRead) {
+			return;
+		}
+		// read the metadata file
+		readMeta();
+	}
+
+	/* (non-Javadoc)
+	 * @see java.lang.Comparable#compareTo(java.lang.Object)
+	 */
+	public int compareTo(Object o) {
+		// TODO Auto-generated method stub
+		return (getName().compareTo(o));
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/DocuDirCache.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,240 @@
+/*
+ * DocuDirCache.java
+ * 
+ * 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 03.03.2003
+ */
+
+package digilib.io;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Cache of digilib directories.
+ * 
+ * @author casties
+ */
+public class DocuDirCache {
+
+	/** general logger for this class */
+	protected static Logger logger = Logger.getLogger(DocuDirCache.class);
+
+	/** Map of directories */
+	Map map = null;
+
+	/** number of files in the whole cache (approximate) */
+	long numFiles = 0;
+
+	/** number of cache hits */
+	long hits = 0;
+
+	/** number of cache misses */
+	long misses = 0;
+
+	/** the root directory element */
+	public DigiDirectory rootDir = null;
+
+	/**
+	 * Default constructor.
+	 */
+	public DocuDirCache() {
+		map = new HashMap();
+		// add root directory
+		rootDir = new DigiDirectory(FileOps.getBaseDirs()[0], "", null);
+		map.put("", rootDir);
+	}
+
+	/**
+	 * The number of directories in the cache.
+	 * 
+	 * @return
+	 */
+	public int size() {
+		return (map != null) ? map.size() : 0;
+	}
+
+	/**
+	 * Add a DocuDirectory to the cache.
+	 * 
+	 * @param newdir
+	 */
+	public void put(DigiDirectory newdir) {
+		String s = newdir.getDLPath();
+		Object oldkey = map.put(s, newdir);
+		if (oldkey != null) {
+			logger.warn("Duplicate key in DocuDirCache.put -- overwritten!");
+		}
+		numFiles += newdir.getSize();
+	}
+
+	/**
+	 * Add a directory to the cache and check its parents.
+	 * 
+	 * @param newDir
+	 */
+	public synchronized void putDir(DigiDirectory newDir) {
+		put(newDir);
+	}
+
+	/**
+	 * Returns a DigiDirectory from the cache.
+	 * 
+	 * @param path
+	 * @return
+	 */
+	public DigiDirectory get(String path) {
+		return (DigiDirectory) map.get(path);
+	}
+
+	/**
+	 * Returns the DigiDirectory indicated by the pathname <code>fn</code>.
+	 * 
+	 * If <code>fn</code> is a file then its parent directory is returned.
+	 * 
+	 * @param fn
+	 *            digilib pathname
+	 * @return
+	 */
+	public DigiDirectory getDirectory(String pn) {
+		/*
+		 * first, assume pn is a directory and look in the cache
+		 */
+		DigiDirectory dd = (DigiDirectory) map.get(pn);
+		if (dd != null) {
+			// cache hit
+			hits++;
+			return dd;
+		} else {
+			/*
+			 * maybe it's a file? try the parent directory
+			 */
+			String dn = FileOps.dlParent(pn);
+			// try it in the cache
+			dd = (DigiDirectory) map.get(dn);
+			if (dd != null) {
+				// cache hit
+				hits++;
+				return dd;
+			} else {
+				// cache miss
+				misses++;
+				/*
+				 * try to read from disk
+				 */
+				File f = FileOps.getRealFile(pn);
+				/*
+				 * is it a directory?
+				 */
+				if (f.isDirectory()) {
+					dd = new DigiDirectory(f, pn, null);
+					// add to the cache
+					putDir(dd);
+					return dd;
+				} else {
+					/*
+					 * then maybe a file? try the parent as a directory
+					 */
+					File d = FileOps.getRealFile(dn);
+					if (d.isDirectory()) {
+						dd = new DigiDirectory(d, dn, null);
+						// add to the cache
+						putDir(dd);
+						return dd;
+					}
+				}
+			}
+		}
+		/*
+		 * otherwise it's crap
+		 */
+		return null;
+	}
+
+	/**
+	 * Returns the DigiDirent with the pathname <code>fn</code> and the index
+	 * <code>in</code> and the class <code>fc</code>.
+	 * 
+	 * If <code>fn</code> is a file then the corresponding DocuDirent is
+	 * returned and the index is ignored.
+	 * 
+	 * @param pn
+	 *            digilib pathname
+	 * @param in
+	 *            file index
+	 * @param fc
+	 *            file class
+	 * @return
+	 */
+	public DigiDirent getFile(String pn, int in, int fc) {
+		// file number is 1-based, vector index is 0-based
+		int n = in - 1;
+		// get the (parent) directory
+		DigiDirectory dd = getDirectory(pn);
+		if (dd != null) {
+			// get the directory's name
+			String dn = dd.getDLPath();
+			if (dn.equals(pn)) {
+				// return the file at the index
+				return dd.get(n, fc);
+			} else {
+				// then the last part must be the filename
+				String fn = FileOps.dlName(pn);
+				return dd.get(fn);
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * @return long
+	 */
+	public long getNumFiles() {
+		return numFiles;
+	}
+
+	/**
+	 * @return long
+	 */
+	public long getHits() {
+		return hits;
+	}
+
+	/**
+	 * @return long
+	 */
+	public long getMisses() {
+		return misses;
+	}
+
+	/**
+	 * @return Returns the rootDir.
+	 */
+	public DigiDirectory getRootDir() {
+		return rootDir;
+	}
+	/**
+	 * @param rootDir The rootDir to set.
+	 */
+	public void setRootDir(DigiDirectory rootDir) {
+		this.rootDir = rootDir;
+	}
+}
--- a/servlet/src/digilib/io/FileOps.java	Thu Jan 17 15:25:46 2002 +0100
+++ b/servlet/src/digilib/io/FileOps.java	Wed Nov 17 18:17:34 2004 +0100
@@ -1,188 +1,675 @@
-/*  FileOps -- Utility class for file operations
-
-  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
-
-*/
+/*
+ * FileOps -- Utility class for file operations
+ * 
+ * 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;
 
-import java.io.*;
-import java.util.*;
+import java.io.File;
+import java.io.FileFilter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
 
-import digilib.*;
-
+import org.apache.log4j.Logger;
 
 public class FileOps {
 
-  private Utils util = null;
-  public static String[] fileTypes = {
-            "jpg", "image/jpeg",
-            "jpeg", "image/jpeg",
-            "png", "image/png",
-            "gif", "image/gif",
-            "tif", "image/tiff",
-            "tiff", "image/tiff"};
+	private static Logger logger = Logger.getLogger(FileOps.class);
+	
+	/**
+	 * Array of file extensions and corresponding mime-types.
+	 */
+	private static String[][] ft = { { "jpg", "image/jpeg" },
+			{ "jpeg", "image/jpeg" }, { "jp2", "image/jp2" },
+			{ "png", "image/png" }, { "gif", "image/gif" },
+			{ "tif", "image/tiff" }, { "tiff", "image/tiff" },
+			{ "txt", "text/plain" }, { "html", "text/html" },
+			{ "htm", "text/html" }, { "xml", "text/xml" },
+			{ "svg", "image/svg+xml" } };
+
+	public static Map fileTypes;
+
+	public static List imageExtensions;
+
+	public static List textExtensions;
+
+	public static List svgExtensions;
+
+	public static final int CLASS_NONE = -1;
+
+	public static final int CLASS_IMAGE = 0;
+
+	public static final int CLASS_TEXT = 1;
+
+	public static final int CLASS_SVG = 2;
+
+	public static final int NUM_CLASSES = 3;
 
-  public FileOps() {
-    util = new Utils();
-  }
+	public static int[] fileClasses = {};
+	
+	public static int[] fcIndexes = {};
+
+	public static final Integer HINT_BASEDIRS = new Integer(1);
+
+	public static final Integer HINT_FILEEXT = new Integer(2);
+
+	public static final Integer HINT_DIRS = new Integer(3);
+
+	public static File[] baseDirs = {};
+	
+	public static DocuDirCache cache;
 
-  public FileOps(Utils u) {
-    util = u;
-  }
+	/**
+	 * static initializer for FileOps
+	 */
+	static {
+		fileTypes = new HashMap();
+		imageExtensions = new ArrayList();
+		textExtensions = new ArrayList();
+		svgExtensions = new ArrayList();
+		// iterate through file types in ft and fill the Map and Lists
+		for (int i = 0; i < ft.length; i++) {
+			String ext = ft[i][0];
+			String mt = ft[i][1];
+			fileTypes.put(ext, mt);
+			if (classForMimetype(mt) == CLASS_IMAGE) {
+				imageExtensions.add(ext);
+			} else if (classForMimetype(mt) == CLASS_TEXT) {
+				textExtensions.add(ext);
+			} else if (classForMimetype(mt) == CLASS_SVG) {
+				svgExtensions.add(ext);
+			}
+		}
+	}
 
-  public void setUtils(Utils u) {
-    util = u;
-  }
+	/** sets the array of actually used file classes */
+	public static void setFileClasses(int[] fc) {
+		fileClasses = fc;
+		fcIndexes = new int[NUM_CLASSES];
+		for (int i = 0; i < fc.length; i++) {
+			fcIndexes[fc[i]] = i;
+		}
+	}
 
+	/** returns the array of actually used file classes */
+	public static int[] getFileClasses() {
+		return fileClasses;
+	}
+
+	/** returns an element from the array of actally used file classes */
+	public static int getFileClass(int idx) {
+		try {
+			return fileClasses[idx];
+		} catch (Exception e) {
+		}
+		return CLASS_NONE;
+	}
 
-  /**
-   *  get the mime type for a file format (by extension)
-   */
-  public static String mimeForFile(File f) {
-    String fn = f.getName();
-    for (int i = 0; i < fileTypes.length; i += 2) {
-      if (fn.toLowerCase().endsWith(fileTypes[i])) {
-        return fileTypes[i+1];
-      }
-    }
-    return null;
-  }
+	/** Returns the index number for the given file class.
+	 * @param fc
+	 * @return
+	 */
+	public static int getFCindex(int fc) {
+		try {
+			return fcIndexes[fc];
+		} catch (Exception e) {
+		}
+		return -1;
+	}
+	
+	/**
+	 * returns the file class for a mime-type
+	 * 
+	 * @param mt
+	 * @return
+	 */
+	public static int classForMimetype(String mt) {
+		if (mt == null) {
+			return CLASS_NONE;
+		}
+		if (mt.startsWith("image/svg")) {
+			return CLASS_SVG;
+		} else if (mt.startsWith("image")) {
+			return CLASS_IMAGE;
+		} else if (mt.startsWith("text")) {
+			return CLASS_TEXT;
+		}
+		return CLASS_NONE;
+	}
+
+	/**
+	 * get the mime type for a file format (by extension)
+	 */
+	public static String mimeForFile(File f) {
+		return (String) fileTypes.get(extname(f.getName().toLowerCase()));
+	}
+
+	/**
+	 * get the file class for the filename (by extension)
+	 * 
+	 * @param fn
+	 * @return
+	 */
+	public static int classForFilename(String fn) {
+		String mt = (String) fileTypes.get(extname(fn).toLowerCase());
+		return classForMimetype(mt);
+	}
 
-  /**
-   *  get a filehandle for a file or directory name
-   *    returns File number n if fn is directory (starts with 1)
-   */
-  public File getFile(String fn, int n) throws FileOpException {
-    util.dprintln(4, "getFile ("+fn+", "+n+")");
+	/**
+	 * get the file class for the file (by extension)
+	 * 
+	 * @param fn
+	 * @return
+	 */
+	public static int classForFile(File f) {
+		return classForFilename(f.getName());
+	}
+
+	public static Iterator getImageExtensionIterator() {
+		return imageExtensions.iterator();
+	}
+
+	public static Iterator getTextExtensionIterator() {
+		return textExtensions.iterator();
+	}
+
+	public static Iterator getSVGExtensionIterator() {
+		return svgExtensions.iterator();
+	}
+
+	/**
+	 * convert a string with a list of pathnames into an array of strings using
+	 * the system's path separator string
+	 */
+	public static String[] pathToArray(String paths) {
+		// split list into directories
+		StringTokenizer dirs = new StringTokenizer(paths, File.pathSeparator);
+		int n = dirs.countTokens();
+		if (n < 1) {
+			return null;
+		}
+		// add directories into array
+		String[] pathArray = new String[n];
+		for (int i = 0; i < n; i++) {
+			String s = dirs.nextToken();
+			// make shure the dir name ends with a directory separator
+			if (s.endsWith(File.separator)) {
+				pathArray[i] = s;
+			} else {
+				pathArray[i] = s + File.separator;
+			}
+		}
+		return pathArray;
+	}
+
+	/**
+	 * Extract the base of a file name (sans extension).
+	 * 
+	 * Returns the filename without the extension. The extension is the part
+	 * behind the last dot in the filename. If the filename has no dot the full
+	 * file name is returned.
+	 * 
+	 * @param fn
+	 * @return
+	 */
+	public static String basename(String fn) {
+		if (fn == null) {
+			return null;
+		}
+		int i = fn.lastIndexOf('.');
+		if (i > 0) {
+			return fn.substring(0, i);
+		}
+		return fn;
+	}
 
-    File f = new File(fn);
-    // if fn is a file name then return file
-    if (f.isFile()) {
-      return f;
-    }
-    // if fn is a directory name then open directory
-    if (f.isDirectory()) {
-      File[] fl = f.listFiles(new ImgFileFilter());
-      Arrays.sort(fl);
-      if ((n > 0) && (n <= fl.length)) {
-         return fl[n - 1];
-      }
-    }
-    throw new FileOpException("Unable to find file: "+fn);
-  }
+	/**
+	 * Extract the base of a file name (sans extension).
+	 * 
+	 * Returns the filename without the extension. The extension is the part
+	 * behind the last dot in the filename. If the filename has no dot the full
+	 * file name is returned.
+	 * 
+	 * @param f
+	 * @return
+	 */
+	public static String basename(File f) {
+		if (f == null) {
+			return null;
+		}
+		return basename(f.getName());
+	}
+
+	/**
+	 * Extract the extension of a file name.
+	 * 
+	 * Returns the extension of a file name. The extension is the part behind
+	 * the last dot in the filename. If the filename has no dot the empty string
+	 * is returned.
+	 * 
+	 * @param fn
+	 * @return
+	 */
+	public static String extname(String fn) {
+		if (fn == null) {
+			return null;
+		}
+		int i = fn.lastIndexOf('.');
+		if (i > 0) {
+			return fn.substring(i + 1);
+		}
+		return "";
+	}
 
-  /**
-   *  get the number of files in a directory
-   *    (almost the same as getFile)
-   *  returns 0 in case of problems
-   */
-  public int getNumFiles(String fn) throws FileOpException {
-    util.dprintln(4, "getNumFiles ("+fn+")");
+	/**
+	 * Extract the parent directory of a (digilib) path name.
+	 * 
+	 * Returns the parent directory of a path name. The parent is the part
+	 * before the last slash in the path name. If the path name has no slash the
+	 * empty string is returned.
+	 * 
+	 * @param fn
+	 * @return
+	 */
+	public static String dlParent(String fn) {
+		if (fn == null) {
+			return null;
+		}
+		int i = fn.lastIndexOf('/');
+		if (i > 0) {
+			return fn.substring(0, i);
+		}
+		return "";
+	}
 
-    File f = new File(fn);
-    // if fn is a file name then return 1
-    if (f.isFile()) {
-      return 1;
-    }
-    // if fn is a directory name then return the number of files
-    if (f.isDirectory()) {
-      return f.listFiles(new ImgFileFilter()).length;
-    }
-    // then fn must be something strange...
-    return 0;
-  }
+	/**
+	 * Extract the dir/file name of a (digilib) path name.
+	 * 
+	 * The file/dir name is the part after the last slash in the path name. If
+	 * the path name has no slash the same string is returned.
+	 * 
+	 * @param path
+	 * @return
+	 */
+	public static String dlName(String path) {
+		if (path == null) {
+			return null;
+		}
+		int i = path.lastIndexOf('/');
+		if (i > 0) {
+			return path.substring(i+1);
+		}
+		return path;
+	}
 
+	/**
+	 * Normalize a path name.
+	 * 
+	 * Removes leading and trailing slashes. Returns null if there is other
+	 * unwanted stuff in the path name.
+	 * 
+	 * @param pathname
+	 * @return
+	 */
+	public static String normalName(String pathname) {
+		if (pathname == null) {
+			return null;
+		}
+		// upper-dir references are unwanted
+		if (pathname.indexOf("../") >= 0) {
+			return null;
+		}
+		int a = 0;
+		int e = pathname.length() - 1;
+		if (e < 0) {
+			return pathname;
+		}
+		// leading and trailing "/" are removed
+		while ((a <= e) && (pathname.charAt(a) == '/')) {
+			a++;
+		}
+		while ((a < e) && (pathname.charAt(e) == '/')) {
+			e--;
+		}
+		return pathname.substring(a, e + 1);
+	}
 
-  /**
-   *  get a filehandle for a file or directory name out of a list
-   *    dirs is a list of base directories, fn is the appended file/dirname
-   *    searches dirs until fn exists (backwards if fwd is false)
-   *    returns File number n if fn is directory (starts with 1)
-   */
-  public File getFileVariant(String[] dirs, String fn, int n, boolean fwd) throws FileOpException {
-    util.dprintln(4, "getVariantFile ("+dirs+", "+fn+", "+n+")");
+	public static StringTokenizer dlPathIterator(String path) {
+		return new StringTokenizer(path, "/");
+	}
+	
+	
+	/**
+	 * FileFilter for general files
+	 */
+	static class ReadableFileFilter implements FileFilter {
+
+		public boolean accept(File f) {
+			return f.canRead();
+		}
+	}
+
+	/**
+	 * FileFilter for image types (helper class for getFile)
+	 */
+	static class ImageFileFilter implements FileFilter {
+
+		public boolean accept(File f) {
+			return (classForFilename(f.getName()) == CLASS_IMAGE);
+		}
+	}
+
+	/**
+	 * FileFilter for text types (helper class for getFile)
+	 */
+	static class TextFileFilter implements FileFilter {
+
+		public boolean accept(File f) {
+			return (classForFilename(f.getName()) == CLASS_TEXT);
+		}
+	}
+
+	/**
+	 * FileFilter for svg types (helper class for getFile).
+	 *  
+	 */
+	static class SVGFileFilter implements FileFilter {
 
-    File f = null;
-    int start = 0;
-    int inc = 1;
-    int end = dirs.length;
-    if (fwd == false) {
-      start = dirs.length - 1;
-      inc = -1;
-      end = 0;
-    }
+		public boolean accept(File f) {
+			return (classForFilename(f.getName()) == CLASS_SVG);
+		}
+	}
+
+	/**
+	 * Factory for FileFilters (image or text).
+	 * 
+	 * @param fileClass
+	 * @return
+	 */
+	public static FileFilter filterForClass(int fileClass) {
+		if (fileClass == CLASS_IMAGE) {
+			return new ImageFileFilter();
+		}
+		if (fileClass == CLASS_TEXT) {
+			return new TextFileFilter();
+		}
+		if (fileClass == CLASS_SVG) {
+			return new SVGFileFilter();
+		}
+		return null;
+	}
 
-    for (int i = start; i != end; i += inc) {
-      try {
-        f = getFile(dirs[i]+fn, n);
-      } catch (FileOpException e) {
-        f = null;
-      }
-      if (f != null) {
-        return f;
-      }
-    }
-    throw new FileOpException("Unable to find file: "+fn);
-  }
+	/**
+	 * Factory for DocuDirents based on file class.
+	 * 
+	 * Returns an ImageFileset, TextFile or SVGFile.
+	 * 
+	 * @param fileClass
+	 * @param file
+	 * @param parent
+	 * @param hints
+	 *            optional additional parameters
+	 * @return
+	 */
+	public static DigiDirent fileForClass(int fileClass, File file,
+			DigiDirectory parent, Map hints) {
+		// what class of file do we have?
+		if (fileClass == CLASS_IMAGE) {
+			// image file
+			return new ImageFileset(file, parent, hints);
+		} else if (fileClass == CLASS_TEXT) {
+			// text file
+			return new TextFile(file, parent);
+		} else if (fileClass == CLASS_SVG) {
+			// text file
+			return new SVGFile(file, parent);
+		}
+		// anything else is a generic dir or file
+		if (file.isDirectory()) {
+			return getCachedDirectory(file, null, parent);
+		}
+		return new DigiDirent(file.getName(), parent);
+	}
+
+	/**
+	 * Filters a list of Files through a FileFilter.
+	 * 
+	 * @param files
+	 * @param filter
+	 * @return
+	 */
+	public static File[] listFiles(File[] files, FileFilter filter) {
+		if (files == null) {
+			return null;
+		}
+		File[] ff = new File[files.length];
+		int ffi = 0;
+		for (int i = 0; i < files.length; i++) {
+			if (filter.accept(files[i])) {
+				ff[ffi] = files[i];
+				ffi++;
+			}
+		}
+		File[] fff = new File[ffi];
+		System.arraycopy(ff, 0, fff, 0, ffi);
+		return fff;
+	}
 
-  /**
-   *  get the number of files in a directory
-   *    (almost the same as getFileVariant)
-   *  returns 0 in case of problems
-   */
-  public int getNumFilesVariant(String[] dirs, String fn, boolean fwd) throws FileOpException {
-    util.dprintln(4, "getNumFilesVariant ("+dirs+", "+fn+")");
+	/**
+	 * Returns the closest matching file out of an array.
+	 * 
+	 * Compares the files sans extensions if no direct match is found. Returns
+	 * null if no match is found.
+	 * 
+	 * @param fn
+	 * @param files
+	 * @return
+	 */
+	public static File findFile(File fn, File[] files) {
+		// try the same filename as the original
+		int fileIdx = Arrays.binarySearch(files, fn);
+		if (fileIdx >= 0) {
+			return files[fileIdx];
+		} else {
+			// try closest matches without extension
+			String fb = FileOps.basename(fn);
+			fileIdx = -fileIdx - 1;
+			if ((fileIdx < files.length)
+					&& (FileOps.basename(files[fileIdx]).equals(fb))) {
+				// idx ok
+				return files[fileIdx];
+			} else if ((fileIdx > 0)
+					&& (FileOps.basename(files[fileIdx - 1]).equals(fb))) {
+				// idx-1 ok
+				return files[fileIdx - 1];
+			} else if ((fileIdx + 1 < files.length)
+					&& (FileOps.basename(files[fileIdx + 1]).equals(fb))) {
+				// idx+1 ok
+				return files[fileIdx + 1];
+			}
+		}
+		// unknown
+		return null;
+	}
 
-    int nf = 0;
-    int start = 0;
-    int inc = 1;
-    int end = dirs.length;
-    if (fwd == false) {
-      start = dirs.length - 1;
-      inc = -1;
-      end = 0;
-    }
+	/**
+	 * Returns the closest matching file out of an array.
+	 * 
+	 * Compares the files sans extensions if no direct match is found. Returns
+	 * null if no match is found.
+	 * 
+	 * @param fn
+	 * @param files
+	 * @return
+	 */
+	public static String findFilename(String fn, String[] files) {
+		// try the same filename as the original
+		int fileIdx = Arrays.binarySearch(files, fn);
+		if (fileIdx >= 0) {
+			return files[fileIdx];
+		} else {
+			// try closest matches without extension
+			String fb = FileOps.basename(fn);
+			fileIdx = -fileIdx - 1;
+			if ((fileIdx < files.length)
+					&& (FileOps.basename(files[fileIdx]).equals(fb))) {
+				// idx ok
+				return files[fileIdx];
+			} else if ((fileIdx > 0)
+					&& (FileOps.basename(files[fileIdx - 1]).equals(fb))) {
+				// idx-1 ok
+				return files[fileIdx - 1];
+			} else if ((fileIdx + 1 < files.length)
+					&& (FileOps.basename(files[fileIdx + 1]).equals(fb))) {
+				// idx+1 ok
+				return files[fileIdx + 1];
+			}
+		}
+		// unknown
+		return null;
+	}
+	/**
+	 * Returns a File for a base directory and a digilib-path.
+	 * 
+	 * @param basedir
+	 * @param dlpath
+	 * @return
+	 */
+	public static File getRealFile(File basedir, String dlpath) {
+		// does this work on all platforms??
+		return new File(basedir, dlpath);
+	}
+
+	/** Returns a File for a digilib-path.
+	 * 
+	 * The file is assumed to be in the first base directory.
+	 * 
+	 * @param dlpath
+	 * @return
+	 */
+	public static File getRealFile(String dlpath) {
+		// does this work on all platforms??
+		return new File(baseDirs[0], dlpath);
+	}
 
-    for (int i = start; i != end; i += inc) {
-      try {
-        nf = getNumFiles(dirs[i]+fn);
-      } catch (FileOpException e) {
-        nf = 0;
-      }
-      if (nf > 0) {
-        return nf;
-      }
-    }
-    return 0;
-  }
+	/**
+	 * Creates a new empty hints Map.
+	 * 
+	 * @return
+	 */
+	public static Map newHints() {
+		Map m = new HashMap();
+		return m;
+	}
+
+	/**
+	 * Creates a new hints Map with the given first element.
+	 * 
+	 * @param type
+	 * @param value
+	 * @return
+	 */
+	public static Map newHints(Integer type, Object value) {
+		Map m = new HashMap();
+		if (type != null) {
+			m.put(type, value);
+		}
+		return m;
+	}
+
+	/**
+	 * @return Returns the baseDirs.
+	 */
+	public static File[] getBaseDirs() {
+		return baseDirs;
+	}
+
+	/**
+	 * @param baseDirs
+	 *            The baseDirs to set.
+	 */
+	public static void setBaseDirs(File[] baseDirs) {
+		FileOps.baseDirs = baseDirs;
+	}
 
-  /**
-   *  FileFilter for image types (helper class for getFile)
-   */
-  private class ImgFileFilter implements FileFilter {
-
-    public boolean accept(File f) {
-      if (f.isFile()) {
-        return (mimeForFile(f) != null);
-      } else {
-        return false;
-      }
-    }
-  }
-
+	
+	/**
+	 * Returns a DigiDirectory instance that is guaranteed to be unique in the
+	 * cache.
+	 * 
+	 * @param dir
+	 * @param parent
+	 * @return
+	 */
+	public static DigiDirectory getCachedDirectory(File dir, String dlpath, DigiDirectory parent) {
+		if (dir == null) {
+			dir = FileOps.getRealFile(dlpath);
+		}
+		DigiDirectory dd = null;
+		if (parent == null) {
+			// create a new parent by starting at the root
+			StringBuffer ps = new StringBuffer();
+			DigiDirectory p = cache.getRootDir();
+			// walk the path
+			for (StringTokenizer i = dlPathIterator(dlpath); i.hasMoreTokens();) {
+				p.check();
+				String dn = i.nextToken();
+				ps.append("/");
+				ps.append(dn);
+				DigiDirectory d = cache.get(dn);
+				if (d == null) {
+					dd = new DigiDirectory(FileOps.getRealFile(dn), ps.toString(), p);
+				}
+				if (d.getParent() != p) {
+					logger.warn("digidirectory "+d.getDLPath()+" has wrong parent: "+p.getDLPath());
+				}
+				p = d;
+			}
+		} else {
+			if (dlpath == null) {
+				dlpath = parent.getDLPath() + "/" + dir.getName();
+			}
+			dd = cache.get(dlpath);
+			if (dd == null) {
+				dd = new DigiDirectory(dir, dlpath, parent);
+			} else {
+				logger.debug("reusing directory:" + dlpath);
+			}
+		}
+		return dd;
+	}
+	
+	/**
+	 * @return Returns the cache.
+	 */
+	public static DocuDirCache getCache() {
+		return cache;
+	}
+	/**
+	 * @param cache The cache to set.
+	 */
+	public static void setCache(DocuDirCache cache) {
+		FileOps.cache = cache;
+	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/ImageFile.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,99 @@
+/* ImageFile.java -- digilib image file 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 25.02.2003
+ */
+ 
+package digilib.io;
+
+import java.io.File;
+import java.io.IOException;
+
+import digilib.image.ImageOps;
+import digilib.image.ImageSize;
+
+/**
+ * @author casties
+ */
+public class ImageFile extends File {
+	
+	private static final long serialVersionUID = 1L;
+	
+	/** file mime-type */
+	private String mimetype = null;
+	
+	/** image size in pixels */
+	private ImageSize pixelSize = null;
+
+	/**
+	 * @param pathname
+	 */
+	public ImageFile(String pathname) {
+		super(pathname);
+	}
+
+	/**
+	 * @param file
+	 */
+	public ImageFile(File file) {
+		super(file.getPath());
+	}
+
+	/**
+	 * @return ImageSize
+	 */
+	public ImageSize getSize() {
+		return pixelSize;
+	}
+
+	/**
+	 * Sets the imageSize.
+	 * @param imageSize The imageSize to set
+	 */
+	public void setSize(ImageSize imageSize) {
+		this.pixelSize = imageSize;
+	}
+
+	/**
+	 * @return String
+	 */
+	public String getMimetype() {
+		return mimetype;
+	}
+
+	/**
+	 * Sets the mimetype.
+	 * @param mimetype The mimetype to set
+	 */
+	public void setMimetype(String filetype) {
+		this.mimetype = filetype;
+	}
+
+	/**
+	 * Checks image size.
+	 * @throws IOException
+	 *  
+	 */
+	public void check() throws IOException {
+		if (pixelSize != null) {
+			return;
+		}
+		ImageOps.checkFile(this);
+	}
+	
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/ImageFileset.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,326 @@
+/* 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.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import digilib.image.ImageOps;
+import digilib.image.ImageSize;
+
+/**
+ * @author casties
+ */
+public class ImageFileset extends DigiDirent {
+
+	/** this is an image file */
+	protected static int FILE_CLASS = FileOps.CLASS_IMAGE;
+
+	/** list of files (ImageFile) */
+	private List files;
+
+	/** resolution of the biggest image (DPI) */
+	private float resX = 0;
+
+	/** resolution of the biggest image (DPI) */
+	private float resY = 0;
+
+	/**
+	 * Constructor with a file and hints.
+	 * 
+	 * The hints are expected to contain 'basedirs' and 'scaledfilext' keys.
+	 * 
+	 * @param file
+	 * @param hints
+	 */
+	public ImageFileset(File file, DigiDirectory parent, Map hints) {
+		super(file.getName(), parent);
+		files = new ArrayList(FileOps.getBaseDirs().length);
+		fill(file, hints);
+	}
+
+	/**
+	 * Gets the default File.
+	 * 
+	 */
+	public ImageFile getFile() {
+		return (files != null) ? (ImageFile) files.get(0) : null;
+	}
+
+	/**
+	 * Get the ImageFile at the index.
+	 * 
+	 * 
+	 * @param index
+	 * @return
+	 */
+	public ImageFile get(int index) {
+		return (files != null) ? (ImageFile) files.get(index) : null;
+	}
+
+	/**
+	 * 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 i = getHiresIterator(); i.hasNext();) {
+			ImageFile f = (ImageFile) i.next();
+			try {
+				ImageOps.checkFile(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 i = getLoresIterator(); i.hasPrevious();) {
+			ImageFile f = (ImageFile) i.previous();
+			try {
+				ImageOps.checkFile(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 (ImageFile) files.get(0);
+	}
+
+	/**
+	 * Returns the biggest ImageFile in the set.
+	 * 
+	 * 
+	 * @return
+	 */
+	public ImageFile getSmallest() {
+		return (ImageFile) files.get(files.size() - 1);
+	}
+
+	/**
+	 * Get an Iterator for this Fileset starting at the highest resolution
+	 * images.
+	 * 
+	 * 
+	 * @return
+	 */
+	public ListIterator getHiresIterator() {
+		return files.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 getLoresIterator() {
+		return files.listIterator(files.size());
+	}
+
+	/**
+	 * Fill the ImageFileset with files from different base directories.
+	 * 
+	 * 
+	 * @param dirs
+	 *            list of base directories
+	 * @param imf
+	 *            file (from first base dir)
+	 * @param hints
+	 * 
+	 */
+	void fill(File imf, Map hints) {
+		File[][] scaledirs = (File[][]) hints.get(FileOps.HINT_BASEDIRS);
+		File[] bd = FileOps.getBaseDirs();
+		int nb = bd.length;
+		if (scaledirs == null) {
+			// read all scaled directories
+			scaledirs = new File[nb][];
+			for (int i = 1; i < nb; i++) {
+				// check basedir + digilib path
+				File d = FileOps.getRealFile(bd[i], parent.getDLPath());
+				scaledirs[i] = d.listFiles();
+			}
+			hints.put(FileOps.HINT_BASEDIRS, scaledirs);
+		}
+		// add the first ImageFile to the ImageFileset
+		files.add(new ImageFile(imf));
+		// iterate the remaining base directories
+		for (int dirIdx = 1; dirIdx < nb; dirIdx++) {
+			if (scaledirs[dirIdx] == null) {
+				continue;
+			}
+			// find the file in the directory
+			File fn = FileOps.findFile(imf, scaledirs[dirIdx]);
+			if (fn == null) {
+				continue;
+			}
+			if (FileOps.classForFile(fn) == FileOps.CLASS_IMAGE) {
+				// add to the fileset
+				files.add(new ImageFile(fn));
+			}
+		}
+	}
+
+	/**
+	 * Reads metadata and sets resolution in resX and resY.
+	 * 
+	 */
+	public void readMeta() {
+		if (isMetaRead) {
+			return;
+		}
+		// read the metadata file
+		super.readMeta();
+		if (meta == null) {
+			// try directory metadata
+			meta = parent.getMeta();
+			if (meta == null) {
+				// no metadata available
+				isMetaRead = true;
+				return;
+			}
+		}
+		isMetaRead = true;
+		String s;
+		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 (meta.containsKey("original-dpi")) {
+			try {
+				dpi = Float.parseFloat((String) meta.get("original-dpi"));
+			} catch (NumberFormatException e) {
+			}
+			if (dpi != 0) {
+				resX = dpi;
+				resY = dpi;
+				return;
+			}
+		}
+		// DPI-X and DPI-Y
+		if (meta.containsKey("original-dpi-x")
+				&& meta.containsKey("original-dpi-y")) {
+			try {
+				dpix = Float.parseFloat((String) meta.get("original-dpi-x"));
+				dpiy = Float.parseFloat((String) meta.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 (meta.containsKey("original-size-x")
+				&& meta.containsKey("original-size-y")
+				&& meta.containsKey("original-pixel-x")
+				&& meta.containsKey("original-pixel-y")) {
+			try {
+				sizex = Float.parseFloat((String) meta.get("original-size-x"));
+				sizey = Float.parseFloat((String) meta.get("original-size-y"));
+				pixx = Float.parseFloat((String) meta.get("original-pixel-x"));
+				pixy = Float.parseFloat((String) meta.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;
+			}
+		}
+	}
+
+	/**
+	 * Returns the aspect ratio of the images.
+	 * 
+	 * @return
+	 */
+	public float getAspect() {
+		for (Iterator i = files.iterator(); i.hasNext();) {
+			ImageFile f = (ImageFile) i.next();
+			ImageSize s = f.getSize();
+			if (s != null) {
+				return s.getAspect();
+			}
+		}
+		return 0f;
+	}
+
+	/**
+	 * @return
+	 */
+	public float getResX() {
+		return resX;
+	}
+
+	/**
+	 * @return
+	 */
+	public float getResY() {
+		return resY;
+	}
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/SVGFile.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,71 @@
+/* SVGFile -- Class for SVG files
+
+  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 25.11.2003 by casties
+ *
+ */
+package digilib.io;
+
+import java.io.File;
+
+
+/** Class for SVG files.
+ * 
+ * @author casties
+ *
+ */
+public class SVGFile extends DigiDirent {
+	/** this is a SVG file */
+	protected static final int FILE_CLASS = FileOps.CLASS_SVG;
+
+	protected File file;
+	
+	/**
+	 * @param name
+	 * @param parent
+	 * @param file
+	 */
+	public SVGFile(File file, DigiDirectory parent) {
+		super(file.getName(), parent);
+		this.file = file;
+	}
+	
+	/**
+	 * @param name
+	 * @param parent
+	 */
+	public SVGFile(String name, File file, DigiDirectory parent) {
+		super(name, parent);
+		this.file = file;
+	}
+
+	/**
+	 * @return Returns the file.
+	 */
+	public File getFile() {
+		return file;
+	}
+	
+	/**
+	 * @param file The file to set.
+	 */
+	public void setFile(File file) {
+		this.file = file;
+	}
+	
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/io/TextFile.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,70 @@
+/* TextFile.java -- Class for text files
+
+ 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;
+
+/**
+ * Class for text files.
+ * 
+ * @author casties
+ *  
+ */
+public class TextFile extends DigiDirent {
+	/** this is a text file */
+	protected static final int fileClass = FileOps.CLASS_TEXT;
+
+	protected File file;
+
+	/**
+	 * @param name
+	 * @param parent
+	 * @param file
+	 */
+	public TextFile(File file, DigiDirectory parent) {
+		super(file.getName(), parent);
+		this.file = file;
+	}
+	
+	/**
+	 * @param name
+	 * @param parent
+	 */
+	public TextFile(String name, File file, DigiDirectory parent) {
+		super(name, parent);
+		this.file = file;
+	}
+	
+	/**
+	 * @return Returns the file.
+	 */
+	public File getFile() {
+		return file;
+	}
+	
+	/**
+	 * @param file The file to set.
+	 */
+	public void setFile(File file) {
+		this.file = file;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/servlet/DigilibConfiguration.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,257 @@
+/*
+ * DigilibConfiguration -- Holding all parameters for digilib servlet.
+ * 
+ * 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.servlet;
+
+import java.io.File;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.Logger;
+
+import digilib.image.DocuImage;
+import digilib.image.DocuImageImpl;
+import digilib.io.FileOps;
+import digilib.io.XMLListLoader;
+
+/**
+ * Class to hold the digilib servlet configuration parameters. The parameters
+ * can be read from the digilib-config file and be passed to other servlets or
+ * beans. <br>errorImgFileName: image file to send in case of error. <br>
+ * denyImgFileName: image file to send if access is denied. <br>baseDirs:
+ * array of base directories in order of preference (prescaled versions first).
+ * <br>useAuth: use authentication information. <br>authConfPath:
+ * authentication configuration file. <br>... <br>
+ * 
+ * @author casties
+ *  
+ */
+public class DigilibConfiguration extends ParameterMap {
+
+	private static final long serialVersionUID = -6630487070791637120L;
+
+	/** DocuImage class instance */
+	private Class docuImageClass = null;
+
+	/** Log4J logger */
+	private Logger logger = Logger.getLogger("digilib.config");
+
+	/**
+	 * Default constructor defines all parameters and their default values.
+	 *  
+	 */
+	public DigilibConfiguration() {
+		// create HashMap(20)
+		super(20);
+		// we start with a default logger config
+		BasicConfigurator.configure();
+
+		/*
+		 * Definition of parameters and default values. System parameters that
+		 * are not read from config file have a type 's'.
+		 */
+
+		// digilib servlet version
+		newParameter(
+			"servlet.version",
+			digilib.servlet.Scaler.dlVersion,
+			null,
+			's');
+		// configuration file location
+		newParameter("servlet.config.file", null, null, 's');
+		// DocuDirCache instance
+		newParameter("servlet.dir.cache", null, null, 's');
+		// DocuImage class instance
+		newParameter(
+			"servlet.docuimage.class",
+			digilib.image.JAIDocuImage.class,
+			null,
+			's');
+		// AuthOps instance for authentication
+		newParameter("servlet.auth.op", null, null, 's');
+
+		/*
+		 * parameters that can be read from config file have a type 'f'
+		 */
+
+		// image file to send in case of error
+		newParameter(
+			"error-image",
+			new File("/docuserver/images/icons/scalerror.gif"),
+			null,
+			'f');
+		// image file to send if access is denied
+		newParameter(
+			"denied-image",
+			new File("/docuserver/images/icons/denied.gif"),
+			null,
+			'f');
+		// base directories in order of preference (prescaled versions last)
+		String[] bd = { "/docuserver/images", "/docuserver/scaled/small" };
+		newParameter("basedir-list", bd, null, 'f');
+		// use authentication information
+		newParameter("use-authorization", Boolean.FALSE, null, 'f');
+		// authentication configuration file
+		newParameter("auth-file", new File("digilib-auth.xml"), null, 'f');
+		// sending image files as-is allowed
+		newParameter("sendfile-allowed", Boolean.TRUE, null, 'f');
+		// Debug level
+		newParameter("debug-level", new Integer(5), null, 'f');
+		// Type of DocuImage instance
+		newParameter(
+			"docuimage-class",
+			"digilib.image.JAIDocuImage",
+			null,
+			'f');
+		// part of URL used to indicate authorized access
+		newParameter("auth-url-path", "authenticated/", null, 'f');
+		// degree of subsampling on image load
+		newParameter("subsample-minimum", new Float(2f), null, 'f');
+		// default scaling quality
+		newParameter("default-quality", new Integer(1), null, 'f');
+		// use mapping file to translate paths
+		newParameter("use-mapping", Boolean.FALSE, null, 'f');
+		// mapping file location
+		newParameter("mapping-file", new File("digilib-map.xml"), null, 'f');
+		// log4j config file location
+		newParameter("log-config-file", new File("log4j-config.xml"), null, 'f');
+		// maximum destination image size (0 means no limit)
+		newParameter("max-image-size", new Integer(0), null, 'f');
+		// use safe (but slower) directory indexing
+		newParameter("safe-dir-index", Boolean.FALSE, null, 'f');
+		// number of working threads
+		newParameter("worker-threads", new Integer(1), null, 'f');
+
+	}
+
+	/**
+	 * Constructor taking a ServletConfig. Reads the config file location from
+	 * an init parameter and loads the config file. Calls <code>readConfig()</code>.
+	 * 
+	 * @see readConfig()
+	 */
+	public DigilibConfiguration(ServletConfig c) throws Exception {
+		this();
+		readConfig(c);
+	}
+
+	/**
+	 * read parameter list from the XML file in init parameter "config-file"
+	 */
+	public void readConfig(ServletConfig c) throws Exception {
+
+		/*
+		 * Get config file name. The file name is first looked for as an init
+		 * parameter, then in a fixed location in the webapp.
+		 */
+		if (c == null) {
+			// no config no file...
+			return;
+		}
+		String fn = c.getInitParameter("config-file");
+		if (fn == null) {
+			fn = ServletOps.getConfigFile("digilib-config.xml", c);
+			if (fn == null) {
+				logger.fatal("readConfig: no param config-file");
+				throw new ServletException("ERROR: no digilib config file!");
+			}
+		}
+		File f = new File(fn);
+		// setup config file list reader
+		XMLListLoader lilo =
+			new XMLListLoader("digilib-config", "parameter", "name", "value");
+		// read config file into HashMap
+		Map confTable = lilo.loadURL(f.toURL().toString());
+
+		// set config file path parameter
+		setValue("servlet.config.file", f.getCanonicalPath());
+
+		/*
+		 * read parameters
+		 */
+
+		for (Iterator i = confTable.keySet().iterator(); i.hasNext();) {
+			String key = (String) i.next();
+			String val = (String) confTable.get(key);
+			Parameter p = get(key);
+			if (p != null) {
+				if (p.getType() == 's') {
+					// type 's' Parameters are not overwritten.
+					continue;
+				}
+				if (!p.setValueFromString(val)) {
+					/*
+					 * automatic conversion failed -- try special cases
+					 */
+
+					// basedir-list
+					if (key.equals("basedir-list")) {
+						// split list into directories
+						String[] sa = FileOps.pathToArray(val);
+						if (sa != null) {
+							p.setValue(sa);
+						}
+					}
+				}
+			} else {
+				// parameter unknown -- just add
+				newParameter(key, null, val, 'f');
+			}
+		}
+
+	}
+
+	/**
+	 * Creates a new DocuImage instance.
+	 * 
+	 * The type of DocuImage is specified by docuImageType.
+	 * 
+	 * @return DocuImage
+	 */
+	public DocuImage getDocuImageInstance() {
+		DocuImageImpl di = null;
+		try {
+			if (docuImageClass == null) {
+				docuImageClass = Class.forName(getAsString("docuimage-class"));
+			}
+			di = (DocuImageImpl) docuImageClass.newInstance();
+		} catch (Exception e) {
+		}
+		return di;
+	}
+
+	/**
+	 * @return Returns the docuImageClass.
+	 */
+	public Class getDocuImageClass() {
+		return docuImageClass;
+	}
+	/**
+	 * @param docuImageClass The docuImageClass to set.
+	 */
+	public void setDocuImageClass(Class docuImageClass) {
+		this.docuImageClass = docuImageClass;
+	}
+}
--- a/servlet/src/digilib/servlet/DocumentBean.java	Thu Jan 17 15:25:46 2002 +0100
+++ b/servlet/src/digilib/servlet/DocumentBean.java	Wed Nov 17 18:17:34 2004 +0100
@@ -1,198 +1,296 @@
-/* DocumentBean -- Access control bean for JSP
-
-  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
-
-*/
+/*
+ * DocumentBean -- Access control bean for JSP
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 2001, 2002, 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.servlet;
 
+import java.util.List;
 
-import java.util.*;
-import javax.servlet.*;
-import javax.servlet.http.*;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
 
-import digilib.*;
-import digilib.io.*;
-import digilib.auth.*;
+import digilib.auth.AuthOpException;
+import digilib.auth.AuthOps;
+import digilib.io.DigiDirectory;
+import digilib.io.DocuDirCache;
+import digilib.io.FileOps;
+import digilib.io.ImageFileset;
 
-public class DocumentBean implements AuthOps {
+public class DocumentBean {
+
+	// general logger
+	private static Logger logger = Logger.getLogger("digilib.docubean");
+
+	// AuthOps object to check authorization
+	private AuthOps authOp;
 
-  // Utils object for logging
-  private Utils util = new Utils(5);
-  // AuthOps object to check authorization
-  private AuthOps authOp;
-  // FileOps object
-  private FileOps fileOp = new FileOps(util);
+	// use authorization database
+	private boolean useAuthentication = true;
+
+	// path to add for authenticated access
+	private String authURLPath = "";
+
+	// DocuDirCache
+	private DocuDirCache dirCache = null;
+
+	// DigilibConfiguration object
+	private DigilibConfiguration dlConfig;
+
+	// DigilibRequest object
+	private DigilibRequest dlRequest = null;
 
-  // base directories in order of preference (prescaled versions first)
-  private String[] baseDirs = {"/docuserver/scaled/small", "/docuserver/images", "/docuserver/scans/quellen"};
-  // part of URL path to prepend for authenticated access
-  private String authURLpath = "authenticated/";
+	/**
+	 * Constructor for DocumentBean.
+	 */
+	public DocumentBean() {
+		super();
+	}
 
-
-  public DocumentBean() {
-  }
+	public DocumentBean(ServletConfig conf) {
+		try {
+			setConfig(conf);
+		} catch (Exception e) {
+			logger.fatal("ERROR: Unable to read config: ", e);
+		}
+	}
 
-  public void setConfig(ServletConfig conf) throws ServletException {
-    util.dprintln(10, "setConfig");
-    // servletOps takes a ServletConfig to get the config file name
-    ServletOps servletOp = new ServletOps(util, conf);
-    /**
-     *  basedir-list : List of document directories
-     */
-    String bl = servletOp.tryToGetInitParam("basedir-list", null);
-    if ((bl != null)&&(bl.length() > 0)) {
-      // split list into directories
-      StringTokenizer dirs = new StringTokenizer(bl, ":");
-      int n = dirs.countTokens();
-      if (n > 0) {
-        // add directories into array
-        baseDirs = new String[n];
-        for (int i = 0; i < n; i++) {
-          baseDirs[i] = dirs.nextToken();
-        }
-      }
-      util.dprintln(3, "basedir-list: "+bl);
-    }
-    /**
-     *  auth-url-path : part of URL to indicate authenticated access
-     */
-    String au = servletOp.tryToGetInitParam("auth-url-path", null);
-    if ((au != null)&&(au.length() > 0)) {
-      authURLpath = au;
-      util.dprintln(3, "auth-url-path: "+au);
-    }
-    /**
-     *  authentication
-     */
-    try {
-      // DB version
-      //private AuthOps authOp = new DBAuthOpsImpl(util);
-      // XML version
-      String cp = servletOp.tryToGetInitParam("auth-file", "/docuserver/www/digitallibrary/WEB-INF/digilib-auth.xml");
-      util.dprintln(3, "auth-file: "+cp);
-      authOp = new XMLAuthOps(util, cp);
-    } catch (AuthOpException e) {
-      throw new ServletException(e);
-    }
-  }
+	public void setConfig(ServletConfig conf) throws ServletException {
+		logger.debug("setConfig");
+		// get our ServletContext
+		ServletContext context = conf.getServletContext();
+		// see if there is a Configuration instance
+		dlConfig = (DigilibConfiguration) context
+				.getAttribute("digilib.servlet.configuration");
+		if (dlConfig == null) {
+			// create new Configuration
+			throw new ServletException("ERROR: No configuration!");
+		}
+
+		// get cache
+		dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
+
+		/*
+		 * authentication
+		 */
+		useAuthentication = dlConfig.getAsBoolean("use-authorization");
+		authOp = (AuthOps) dlConfig.getValue("servlet.auth.op");
+		authURLPath = dlConfig.getAsString("auth-url-path");
+		if (useAuthentication && (authOp == null)) {
+			throw new ServletException(
+					"ERROR: use-authorization configured but no AuthOp!");
+		}
+	}
+
+	/**
+	 * check if the request must be authorized to access filepath
+	 */
+	public boolean isAuthRequired(DigilibRequest request)
+			throws AuthOpException {
+		logger.debug("isAuthRequired");
+		return useAuthentication ? authOp.isAuthRequired(request) : false;
+	}
+
+	/**
+	 * check if the request is allowed to access filepath
+	 */
+	public boolean isAuthorized(DigilibRequest request) throws AuthOpException {
+		logger.debug("isAuthorized");
+		return useAuthentication ? authOp.isAuthorized(request) : true;
+	}
+
+	/**
+	 * return a list of authorization roles needed for request to access the
+	 * specified path
+	 */
+	public List rolesForPath(DigilibRequest request) throws AuthOpException {
+		logger.debug("rolesForPath");
+		return useAuthentication ? authOp.rolesForPath(request) : null;
+	}
+
+	/**
+	 * check request authorization against a list of roles
+	 */
+	public boolean isRoleAuthorized(List roles, DigilibRequest request) {
+		logger.debug("isRoleAuthorized");
+		return useAuthentication ? authOp.isRoleAuthorized(roles, request)
+				: true;
+	}
+
+	/**
+	 * check for authenticated access and redirect if necessary
+	 */
+	public boolean doAuthentication(HttpServletResponse response)
+			throws Exception {
+		return doAuthentication(dlRequest, response);
+	}
 
-  public String getDocuPath(HttpServletRequest request) {
-    util.dprintln(10, "getDocuPath");
-    // fetch query string
-    String qs = request.getQueryString();
-    String fn = "";
-    if (qs != null && qs.length() > 0) {
-       // the file name is in the request before the first "+"
-       int endfn = qs.indexOf("+");
-       if (endfn > 0) {
-         fn = qs.substring(0, endfn);
-       } else {
-	 fn = qs;
-       }
-    }
-    util.dprintln(4, "docuPath: "+fn);
-    return fn;
-  }
-
-  /**
-   *  check if the request must be authorized to access filepath
-   */
-  public boolean isAuthRequired(HttpServletRequest request) throws AuthOpException {
-    util.dprintln(10, "isAuthRequired");
-    return authOp.isAuthRequired(getDocuPath(request), request);
-  }
+	/**
+	 * check for authenticated access and redirect if necessary
+	 */
+	public boolean doAuthentication(DigilibRequest request,
+			HttpServletResponse response) throws Exception {
+		logger.debug("doAuthentication");
+		if (!useAuthentication) {
+			// shortcut if no authentication
+			return true;
+		}
+		// check if we are already authenticated
+		if (((HttpServletRequest) request.getServletRequest()).getRemoteUser() == null) {
+			logger.debug("unauthenticated so far");
+			// if not maybe we must?
+			if (isAuthRequired(request)) {
+				logger.debug("auth required, redirect");
+				// we are not yet authenticated -> redirect
+				response.sendRedirect(authURLPath
+						+ ((HttpServletRequest) request.getServletRequest())
+								.getServletPath()
+						+ "?"
+						+ ((HttpServletRequest) request.getServletRequest())
+								.getQueryString());
+			}
+		}
+		return true;
+	}
 
-  public boolean isAuthRequired(String filepath, HttpServletRequest request) throws AuthOpException {
-    util.dprintln(10, "isAuthRequired");
-    return authOp.isAuthRequired(filepath, request);
-  }
-
-  /**
-   *  check if the request is allowed to access filepath
-   */
-  public boolean isAuthorized(HttpServletRequest request) throws AuthOpException {
-    util.dprintln(10, "isAuthorized");
-    return authOp.isAuthorized(getDocuPath(request), request);
-  }
+	/**
+	 * Sets the current DigilibRequest. Also completes information in the request.
+	 * 
+	 * @param dlRequest
+	 *            The dlRequest to set.
+	 */
+	public void setRequest(DigilibRequest dlRequest) throws Exception {
+		this.dlRequest = dlRequest;
+		if (dirCache == null) {
+			return;
+		}
+		String fn = dlRequest.getFilePath();
+		// get information about the file
+		ImageFileset fileset = (ImageFileset) dirCache.getFile(fn, dlRequest
+				.getAsInt("pn"), FileOps.CLASS_IMAGE);
+		if (fileset == null) {
+			return;
+		}
+		// add file name
+		dlRequest.setValue("img.fn", fileset.getName());
+		// add dpi
+		fileset.check();
+		dlRequest.setValue("img.dpix", new Double(fileset.getResX()));
+		dlRequest.setValue("img.dpiy", new Double(fileset.getResY()));
+		// get number of pages in directory
+		DigiDirectory dd = dirCache.getDirectory(fn);
+		if (dd != null) {
+			dlRequest.setValue("pt", dd.getSize(FileOps.CLASS_IMAGE));
+		}
+	}
 
-  public boolean isAuthorized(String filepath, HttpServletRequest request) throws AuthOpException {
-    util.dprintln(10, "isAuthorized");
-    return authOp.isAuthorized(filepath, request);
-  }
+	/**
+	 * get the first page number in the directory (not yet functional)
+	 */
+	public int getFirstPage(DigilibRequest request) {
+		logger.debug("getFirstPage");
+		return 1;
+	}
 
-  /**
-   *  return a list of authorization roles needed for request
-   *  to access the specified path
-   */
-  public List rolesForPath(String filepath, HttpServletRequest request) throws AuthOpException {
-    util.dprintln(10, "rolesForPath");
-    return authOp.rolesForPath(filepath, request);
-  }
+	/**
+	 * get the number of pages/files in the directory
+	 */
+	public int getNumPages() throws Exception {
+		return getNumPages(dlRequest);
+	}
 
-  /**
-   * check request authorization against a list of roles
-   */
-  public boolean isRoleAuthorized(List roles, HttpServletRequest request) {
-    util.dprintln(10, "isRoleAuthorized");
-    return authOp.isRoleAuthorized(roles, request);
-  }
+	/**
+	 * get the number of pages/files in the directory
+	 */
+	public int getNumPages(DigilibRequest request) throws Exception {
+		logger.debug("getNumPages");
+		DigiDirectory dd = (dirCache != null) ? dirCache.getDirectory(request
+				.getFilePath()) : null;
+		if (dd != null) {
+			return dd.getSize(FileOps.CLASS_IMAGE);
+		}
+		return 0;
+	}
 
-  /**
-   * check for authenticated access and redirect if necessary
-   */
-  public boolean doAuthentication(HttpServletRequest request, HttpServletResponse response) throws Exception {
-    util.dprintln(10, "doAuthentication");
-    // check if we are already authenticated
-    if (request.getRemoteUser() == null) {
-      util.dprintln(3, "unauthenticated so far");
-      // if not maybe we must?
-      if (isAuthRequired(request)) {
-        util.dprintln(3, "auth required, redirect");
-	// we are not yet authenticated -> redirect
-	response.sendRedirect(authURLpath+request.getServletPath()+"?"+request.getQueryString());
-      }
-    }
-    return true;
-  }
+	/**
+	 * Returns the dlConfig.
+	 * 
+	 * @return DigilibConfiguration
+	 */
+	public DigilibConfiguration getDlConfig() {
+		return dlConfig;
+	}
+
+	/**
+	 * returns if the zoom area in the request can be moved
+	 * 
+	 * @return
+	 */
+	public boolean canMoveRight() {
+		float ww = dlRequest.getAsFloat("ww");
+		float wx = dlRequest.getAsFloat("wx");
+		return (ww + wx < 1.0);
+	}
 
-  /**
-   *  get the first page number in the directory
-   *  (not yet functional)
-   */
-  public int getFirstPage(HttpServletRequest request) {
-    return getFirstPage(getDocuPath(request), request);
-  }
-
-  public int getFirstPage(String filepath, HttpServletRequest request) {
-    util.dprintln(10, "getFirstPage");
-    return 1;
-  }
+	/**
+	 * returns if the zoom area in the request can be moved
+	 * 
+	 * @return
+	 */
+	public boolean canMoveLeft() {
+		float ww = dlRequest.getAsFloat("ww");
+		float wx = dlRequest.getAsFloat("wx");
+		return ((ww < 1.0) && (wx > 0));
+	}
 
-  /**
-   *  get the number of pages/files in the directory
-   */
-  public int getNumPages(HttpServletRequest request) throws Exception {
-    return getNumPages(getDocuPath(request), request);
-  }
+	/**
+	 * returns if the zoom area in the request can be moved
+	 * 
+	 * @return
+	 */
+	public boolean canMoveUp() {
+		float wh = dlRequest.getAsFloat("wh");
+		float wy = dlRequest.getAsFloat("wy");
+		return ((wh < 1.0) && (wy > 0));
+	}
 
-  public int getNumPages(String filepath, HttpServletRequest request) throws Exception {
-    util.dprintln(10, "getNumPages");
-    return fileOp.getNumFilesVariant(baseDirs, "/"+filepath, true);
-  }
+	/**
+	 * returns if the zoom area in the request can be moved
+	 * 
+	 * @return
+	 */
+	public boolean canMoveDown() {
+		float wh = dlRequest.getAsFloat("wh");
+		float wy = dlRequest.getAsFloat("wy");
+		return (wh + wy < 1.0);
+	}
 
-}
+	/**
+	 * @return Returns the dlRequest.
+	 */
+	public DigilibRequest getRequest() {
+		return dlRequest;
+	}
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servlet/src/digilib/servlet/Initialiser.java	Wed Nov 17 18:17:34 2004 +0100
@@ -0,0 +1,175 @@
+/* Initialiser.java -- initalisation servlet for setup tasks
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 2004 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 18.10.2004
+ */
+package digilib.servlet;
+
+import java.io.File;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.xml.DOMConfigurator;
+
+import EDU.oswego.cs.dl.util.concurrent.FIFOSemaphore;
+import EDU.oswego.cs.dl.util.concurrent.Semaphore;
+import digilib.auth.AuthOps;
+import digilib.auth.XMLAuthOps;
+import digilib.io.AliasingDocuDirCache;
+import digilib.io.DocuDirCache;
+import digilib.io.FileOps;
+
+/**
+ * Initalisation servlet for setup tasks.
+ * 
+ * @author casties
+ *  
+ */
+public class Initialiser extends HttpServlet {
+
+	private static final long serialVersionUID = -5126621114382549343L;
+
+	/** servlet version */
+	public static final String iniVersion = "0.1b1";
+
+	/** gengeral logger for this class */
+	private static Logger logger = Logger.getLogger("digilib.init");
+
+	/** AuthOps instance */
+	AuthOps authOp;
+
+	/** DocuDirCache instance */
+	DocuDirCache dirCache;
+
+	/** DigilibConfiguration instance */
+	DigilibConfiguration dlConfig;
+
+	/** use authorization database */
+	boolean useAuthentication = false;
+
+	/**
+	 * Initialisation on first run.
+	 * 
+	 * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
+	 */
+	public void init(ServletConfig config) throws ServletException {
+		super.init(config);
+
+		System.out
+				.println("***** Digital Image Library Initialisation Servlet (version "
+						+ iniVersion + ") *****");
+
+		// 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);
+
+				/*
+				 * further initialization
+				 */
+
+				// set up the logger
+				File logConf = ServletOps.getConfigFile((File) dlConfig
+						.getValue("log-config-file"), config);
+				DOMConfigurator.configure(logConf.getAbsolutePath());
+				dlConfig.setValue("log-config-file", logConf);
+				// say hello in the log file
+				logger
+						.info("***** Digital Image Library Initialisation Servlet (version "
+								+ iniVersion + ") *****");
+				// base directories
+				String[] bd = (String[]) dlConfig.getValue("basedir-list");
+				File[] dirs = new File[bd.length];
+				int ndirs = 0;
+				for (int i = 0; i < bd.length; i++) {
+					File d = new File(bd[i]);
+					if (d.isDirectory()) {
+						dirs[ndirs++] = d;
+					}
+				}
+				File[] bdirs = new File[ndirs];
+				System.arraycopy(dirs, 0, bdirs, 0, ndirs);
+				FileOps.setBaseDirs(bdirs);
+				// file classes
+				int[] fcs = { FileOps.CLASS_IMAGE, FileOps.CLASS_TEXT,
+						FileOps.CLASS_SVG };
+				FileOps.setFileClasses(fcs);
+				// directory cache
+				if (dlConfig.getAsBoolean("use-mapping")) {
+					// with mapping file
+					File mapConf = ServletOps.getConfigFile((File) dlConfig
+							.getValue("mapping-file"), config);
+					dirCache = new AliasingDocuDirCache(mapConf);
+					dlConfig.setValue("mapping-file", mapConf);
+				} else {
+					// without mapping
+					dirCache = new DocuDirCache();
+				}
+				dlConfig.setValue("servlet.dir.cache", dirCache);
+				FileOps.setCache(dirCache);
+				// useAuthentication
+				if (dlConfig.getAsBoolean("use-authorization")) {
+					// DB version
+					//authOp = new DBAuthOpsImpl(util);
+					// XML version
+					File authConf = ServletOps.getConfigFile((File) dlConfig
+							.getValue("auth-file"), config);
+					authOp = new XMLAuthOps(authConf);
+					dlConfig.setValue("servlet.auth.op", authOp);
+					dlConfig.setValue("auth-file", authConf);
+				}
+				// DocuImage class
+				Class cl = Class.forName(dlConfig
+						.getAsString("docuimage-class"));
+				dlConfig.setDocuImageClass(cl);
+				dlConfig.setValue("servlet.docuimage.class", cl.getName());
+				// worker threads
+				int nt = dlConfig.getAsInt("worker-threads");
+				Semaphore lck = new FIFOSemaphore(nt);
+				DigilibWorker.setLock(lck);
+				// set as the servlets main config
+				context.setAttribute("digilib.servlet.configuration", dlConfig);
+
+			} catch (Exception e) {
+				throw new ServletException(e);
+			}
+		} else {
+			// say hello in the log file
+			logger
+					.info("***** Digital Image Library Initialisation Servlet (version "
+							+ iniVersion + ") *****");
+			logger.warn("Already initialised?");
+			// set our AuthOps
+			useAuthentication = dlConfig.getAsBoolean("use-authorization");
+			// AuthOps instance
+			authOp = (AuthOps) dlConfig.getValue("servlet.auth.op");
+			// DocuDirCache instance
+			dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
+		}
+	}
+
+}
--- a/servlet/src/digilib/servlet/Scaler.java	Thu Jan 17 15:25:46 2002 +0100
+++ b/servlet/src/digilib/servlet/Scaler.java	Wed Nov 17 18:17:34 2004 +0100
@@ -1,377 +1,701 @@
-/* Scaler -- Scaler servlet main class
-
-  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
-
-*/
+/*
+ * Scaler -- Scaler servlet main class
+ * 
+ * Digital Image Library servlet components
+ * 
+ * Copyright (C) 200-2004 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.servlet;
 
-import javax.servlet.*;
-import javax.servlet.http.*;
-import java.io.*;
-import java.util.*;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.NoninvertibleTransformException;
+import java.awt.geom.Rectangle2D;
+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.*;
-import digilib.io.*;
-import digilib.image.*;
-import digilib.auth.*;
+import digilib.auth.AuthOpException;
+import digilib.auth.AuthOps;
+import digilib.image.ImageOpException;
+import digilib.image.ImageSize;
+import digilib.io.DigiDirectory;
+import digilib.io.DigiDirent;
+import digilib.io.DocuDirCache;
+import digilib.io.FileOpException;
+import digilib.io.FileOps;
+import digilib.io.ImageFile;
+import digilib.io.ImageFileset;
 
-
+/**
+ * @author casties
+ */
 //public class Scaler extends HttpServlet implements SingleThreadModel {
 public class Scaler extends HttpServlet {
 
-  // Utils instance with debuglevel
-  Utils util;
-  // ServletOpss instance
-  ServletOps servletOp;
-  // FileOps instance
-  FileOps fileOp;
-  // AuthOps instance
-  AuthOps authOp;
-  // global DocuImage instance (don't reuse inside a request!)
-  DocuImage globalImage;
+	private static final long serialVersionUID = -325080527268912852L;
+
+	/** digilib servlet version (for all components) */
+	public static final String dlVersion = "1.6.0a";
+
+	/** logger for accounting requests */
+	private static Logger accountlog = Logger.getLogger("account.request");
+
+	/** gengeral logger for this class */
+	private static Logger logger = Logger.getLogger("digilib.servlet");
+
+	/** logger for authentication related */
+	private static Logger authlog = Logger.getLogger("digilib.auth");
+
+	/** general error code */
+	public static final int ERROR_UNKNOWN = 0;
+
+	/** error code for authentication error */
+	public static final int ERROR_AUTH = 1;
+
+	/** error code for file operation error */
+	public static final int ERROR_FILE = 2;
+
+	/** error code for image operation error */
+	public static final int ERROR_IMAGE = 3;
+	
+	/** servlet start time */
+	long starttime = 0;
+
+	/** DocuDirCache instance */
+	DocuDirCache dirCache;
+
+	/** authentication error image file */
+	File denyImgFile;
+
+	/** image error image file */
+	File errorImgFile;
+
+	/** subsampling before scaling */
+	float minSubsample = 2f;
+
+	/** send files as is? */
+	boolean sendFileAllowed = true;
+
+	/** default scaling quality */
+	int defaultQuality = 1;
+
+	/** DigilibConfiguration instance */
+	DigilibConfiguration dlConfig;
+
+	/** use authorization database */
+	boolean useAuthorization = true;
+
+	/** AuthOps instance */
+	AuthOps authOp;
+
+	// EXPRIMENTAL
+	/** try to enlarge cropping area for "oblique" angles */
+	boolean wholeRotArea = false;
+
+	/**
+	 * Initialisation on first run.
+	 * 
+	 * @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 "
+						+ dlVersion + ") *****");
+		// say hello in the log file
+		logger
+				.info("***** Digital Image Library Image Scaler Servlet (version "
+						+ dlVersion + ") *****");
 
-  // use authorization database
-  boolean useAuthentication = true;
-  // image file to send in case of error
-  File errorImgFile = new File("/docuserver/images/icons/scalerror.gif");
-  // image file to send if access is denied
-  File denyImgFile = new File("/docuserver/images/icons/denied.gif");
-  // base directories in order of preference (prescaled versions first)
-  String[] baseDirs = {"/docuserver/scaled/small", "/docuserver/images", "/docuserver/scans/quellen"};
+		starttime = System.currentTimeMillis();
+		// 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 = (File) dlConfig.getValue("denied-image");
+		errorImgFile = (File) dlConfig.getValue("error-image");
+		sendFileAllowed = dlConfig.getAsBoolean("sendfile-allowed");
+		minSubsample = dlConfig.getAsFloat("subsample-minimum");
+		defaultQuality = dlConfig.getAsInt("default-quality");
+	}
+
+	/** Process the HTTP Get request */
+	public void doGet(HttpServletRequest request, HttpServletResponse response)
+			throws ServletException, IOException {
+		accountlog.info("GET from " + request.getRemoteAddr());
+		// create new request with defaults
+		DigilibRequest dlReq = new DigilibRequest();
+		// set with request parameters
+		dlReq.setWithRequest(request);
+		// add DigilibRequest to ServletRequest
+		request.setAttribute("digilib.servlet.request", dlReq);
+		// do the processing
+		processRequest(request, response);
+	}
 
-  /*********************************************************
-   *             Initialize global variables
-   *********************************************************/
-  public void init(ServletConfig config) throws ServletException {
-    super.init(config);
+	/** Process the HTTP Post request */
+	public void doPost(HttpServletRequest request, HttpServletResponse response)
+			throws ServletException, IOException {
+		accountlog.info("POST from " + request.getRemoteAddr());
+		// create new request with defaults
+		DigilibRequest dlReq = new DigilibRequest();
+		// set with request parameters
+		dlReq.setWithRequest(request);
+		// add DigilibRequest to ServletRequest
+		request.setAttribute("digilib.servlet.request", dlReq);
+		// do the processing
+		processRequest(request, response);
+	}
 
-    // first we need an Utils to setup ServletOps UGLY!!
-    util = new Utils(5);
-    // servletOps takes a ServletConfig to get the config file name
-    servletOp = new ServletOps(util, config);
-    // then we can start reading parameters UGLY!!
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see javax.servlet.http.HttpServlet#getLastModified(javax.servlet.http.HttpServletRequest)
+	 */
+	protected long getLastModified(HttpServletRequest request) {
+		accountlog.debug("GetLastModified from " + request.getRemoteAddr()
+				+ " for " + request.getQueryString());
+		long mtime = -1;
+		// create new request with defaults
+		DigilibRequest dlReq = new DigilibRequest();
+		// set with request parameters
+		dlReq.setWithRequest(request);
+		// find the requested file
+		DigiDirent f = findFile(dlReq);
+		if (f != null) {
+			DigiDirectory dd = f.getParent();
+			mtime = dd.getMtime() / 1000 * 1000;
+		}
+		// limit time to servlet start time
+		if (mtime < starttime) {
+			mtime = starttime;
+		}
+		return mtime;
+	}
+
+	/** main request handler. */
+	void processRequest(HttpServletRequest request, HttpServletResponse response)
+			throws ServletException {
 
-    // Utils instance with debuglevel
-    int debugLevel = servletOp.tryToGetInitParam("debug-level", 10);
-    util = new Utils(debugLevel);
-    // reset Util for ServletOps instance
-    servletOp.setUtils(util);
-    // image file to send in case of error
-    String errorImgFileName = servletOp.tryToGetInitParam("error-image", "/docuserver/images/icons/scalerror.gif");
-    errorImgFile = new File(errorImgFileName);
-    // image file to send if access is denied
-    String denyImgFileName = servletOp.tryToGetInitParam("denied-image", "/docuserver/images/icons/denied.gif");
-    denyImgFile = new File(denyImgFileName);
-    // base directories in order of preference (prescaled versions first)
-    String baseDirList = servletOp.tryToGetInitParam("basedir-list", "/docuserver/scaled/small:/docuserver/images:/docuserver/scans/quellen");
-    // split list into directories
-    StringTokenizer dirs = new StringTokenizer(baseDirList, ":");
-    int n = dirs.countTokens();
-    // add directories into array
-    baseDirs = new String[n];
-    for (int i = 0; i < n; i++) {
-      baseDirs[i] = dirs.nextToken();
-    }
-    // use authentication information
-    String useAuth = servletOp.tryToGetInitParam("use-authorization", "true");
-    if ((useAuth.indexOf("false") > 0)||(useAuth.indexOf("FALSE") > 0)) {
-      useAuthentication = false;
-    } else {
-      useAuthentication = true;
-      try {
-        // DB version
-        //authOp = new DBAuthOpsImpl(util);
-        // XML version
-        String cnfPath = servletOp.tryToGetInitParam("auth-file", "/docuserver/www/digitallibrary/WEB-INF/digilib-auth.xml");
-        authOp = new XMLAuthOps(util, cnfPath);
-      } catch (AuthOpException e) {
-        throw new ServletException(e);
-      }
-    }
-    // FileOps instance
-    fileOp = new FileOps(util);
-    // global DocuImage instance (don't reuse inside a request!)
-    globalImage = new JAIDocuImage(util);
-//    globalImage = new JIMIDocuImage(util);
-    //globalImage = new ImageLoaderDocuImage(util);
+		if (dlConfig == null) {
+			throw new ServletException("ERROR: No Configuration!");
+		}
+
+		accountlog.debug("request: " + request.getQueryString());
+		logger.debug("request: " + request.getQueryString());
+
+		// time for benchmarking
+		long startTime = System.currentTimeMillis();
+		// output mime-type
+		String mimeType = "image/png";
+
+		/* parameters for a session */
 
-  }
+		// scale the image file to fit window size i.e. respect dw,dh
+		boolean scaleToFit = true;
+		// scale the image by a fixed factor only
+		boolean absoluteScale = false;
+		// only crop the image to fit
+		boolean cropToFit = false;
+		// send the file as is
+		boolean sendFile = false;
+		// use low resolution images only
+		boolean loresOnly = false;
+		// use hires images only
+		boolean hiresOnly = false;
+		// send the image always as JPEG
+		boolean forceJPEG = false;
+		// interpolation to use for scaling
+		int scaleQual = defaultQuality;
+		// send html error message (or image file)
+		boolean errorMsgHtml = false;
+		// mirror the image
+		boolean doMirror = false;
+		// angle of mirror axis
+		float mirrorAngle = 0;
+		// original (hires) image resolution
+		float origResX = 0;
+		float origResY = 0;
+
+		/* request parameters */
+
+		DigilibRequest dlRequest = (DigilibRequest) request
+				.getAttribute("digilib.servlet.request");
 
-  /**Process the HTTP Get request*/
-  public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
-    util.dprintln(1, "The servlet has received a GET!");
-    processRequest(request, response);
-  }
-
-  /**Process the HTTP Post request*/
-  public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
-    util.dprintln(1, "The servlet has received a POST!");
-    processRequest(request, response);
-  }
-
-  /**Clean up resources*/
-  public void destroy() {
-  }
-
-/**********************************************************************
- *                       main request handler
- **********************************************************************/
-
-  void processRequest(HttpServletRequest request, HttpServletResponse response)
-    throws ServletException, IOException {
+		// destination image width
+		int paramDW = dlRequest.getAsInt("dw");
+		// destination image height
+		int paramDH = dlRequest.getAsInt("dh");
+		// relative area x_offset (0..1)
+		float paramWX = dlRequest.getAsFloat("wx");
+		// relative area y_offset
+		float paramWY = dlRequest.getAsFloat("wy");
+		// relative area width (0..1)
+		float paramWW = dlRequest.getAsFloat("ww");
+		// relative area height
+		float paramWH = dlRequest.getAsFloat("wh");
+		// scale factor (additional to dw/width, dh/height)
+		float paramWS = dlRequest.getAsFloat("ws");
+		// rotation angle
+		float paramROT = dlRequest.getAsFloat("rot");
+		// contrast enhancement
+		float paramCONT = dlRequest.getAsFloat("cont");
+		// brightness enhancement
+		float paramBRGT = dlRequest.getAsFloat("brgt");
+		// color modification
+		float[] paramRGBM = null;
+		Parameter p = dlRequest.get("rgbm");
+		if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
+			paramRGBM = p.parseAsFloatArray("/");
+		}
+		float[] paramRGBA = null;
+		p = dlRequest.get("rgba");
+		if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
+			paramRGBA = p.parseAsFloatArray("/");
+		}
+		// destination resolution (DPI)
+		float paramDDPIX = dlRequest.getAsFloat("ddpix");
+		float paramDDPIY = dlRequest.getAsFloat("ddpiy");
+		if ((paramDDPIX == 0) || (paramDDPIY == 0)) {
+			// if X or Y resolution isn't set, use DDPI
+			paramDDPIX = dlRequest.getAsFloat("ddpi");
+			paramDDPIY = paramDDPIX;
+		}
 
-    // time for benchmarking
-    long startTime = System.currentTimeMillis();
-    // output mime/type
-    String mimeType = "image/png";
-
-    /**
-     * parameters for a session
-     */
+		/*
+		 * operation mode: "fit": always fit to page, "clip": send original
+		 * resolution cropped, "file": send whole file (if allowed)
+		 */
+		if (dlRequest.hasOption("mo", "clip")) {
+			scaleToFit = false;
+			absoluteScale = false;
+			cropToFit = true;
+			sendFile = false;
+			hiresOnly = true;
+		} else if (dlRequest.hasOption("mo", "fit")) {
+			scaleToFit = true;
+			absoluteScale = false;
+			cropToFit = false;
+			sendFile = false;
+			hiresOnly = false;
+		} else if (dlRequest.hasOption("mo", "osize")) {
+			scaleToFit = false;
+			absoluteScale = true;
+			cropToFit = false;
+			sendFile = false;
+			hiresOnly = true;
+		}
+		// operation mode: "lores": try to use scaled image, "hires": use
+		// unscaled image
+		//   "autores": try best fitting resolution
+		if (dlRequest.hasOption("mo", "lores")) {
+			loresOnly = true;
+			hiresOnly = false;
+		} else if (dlRequest.hasOption("mo", "hires")) {
+			loresOnly = false;
+			hiresOnly = true;
+		} else if (dlRequest.hasOption("mo", "autores")) {
+			loresOnly = false;
+			hiresOnly = false;
+		}
+		// operation mode: "errtxt": error message in html, "errimg": error
+		// image
+		if (dlRequest.hasOption("mo", "errtxt")) {
+			errorMsgHtml = true;
+		} else if (dlRequest.hasOption("mo", "errimg")) {
+			errorMsgHtml = false;
+		}
+		// operation mode: "q0" - "q2": interpolation quality
+		if (dlRequest.hasOption("mo", "q0")) {
+			scaleQual = 0;
+		} else if (dlRequest.hasOption("mo", "q1")) {
+			scaleQual = 1;
+		} else if (dlRequest.hasOption("mo", "q2")) {
+			scaleQual = 2;
+		}
+		// operation mode: "jpg": always use JPEG
+		if (dlRequest.hasOption("mo", "jpg")) {
+			forceJPEG = true;
+		}
 
-    // scale the image file to fit window size
-    boolean scaleToFit = true;
-    // use heuristics (GIF?) to scale or not
-    boolean forcedScale = false;
-    // try prescaled images first
-    boolean preScaledFirst = true;
-    // interpolation to use for scaling
-    int scaleQual = 0;
-    // send html error message (or image file)
-    boolean errorMsgHtml = false;
+		// check with the maximum allowed size (if set)
+		int maxImgSize = dlConfig.getAsInt("max-image-size");
+		if (maxImgSize > 0) {
+			paramDW = (paramDW * paramWS > maxImgSize) ? (int) (maxImgSize / paramWS)
+					: paramDW;
+			paramDH = (paramDH * paramWS > maxImgSize) ? (int) (maxImgSize / paramWS)
+					: paramDH;
+		}
 
-    /**
-     *  request parameter
-     */
+		//"big" try for all file/image actions
+		try {
+
+			// ImageFileset of the image to load
+			ImageFileset fileset = null;
+
+			/* find the file to load/send */
+
+			// get PathInfo
+			String loadPathName = dlRequest.getFilePath();
 
-    // file/dir to load
-    String param_fn = servletOp.tryToGetParam("fn", "", request);
-    // page number
-    int param_pn = servletOp.tryToGetParam("pn", 1, request);
-    // destination image width
-    int param_dw = servletOp.tryToGetParam("dw", 300, request);
-    // destination image height
-    int param_dh = servletOp.tryToGetParam("dh", 400, request);
-    // relative area x_offset (0..1)
-    float param_wx = servletOp.tryToGetParam("wx", 0f, request);
-    // relative area y_offset
-    float param_wy = servletOp.tryToGetParam("wy", 0f, request);
-    // relative area width (0..1)
-    float param_ww = servletOp.tryToGetParam("ww", 1f, request);
-    // relative area height
-    float param_wh = servletOp.tryToGetParam("wh", 1f, request);
-    // scale factor (additional to dw/width, dh/height)
-    float param_ws = servletOp.tryToGetParam("ws", 1f, request);
-    // operation mode: flags separated by "+"
-    String param_mo = servletOp.tryToGetParam("mo", "", request);
-    // operation mode: "fit": always fit to page, "file": send as-is
-    if (param_mo.indexOf("fit") >= 0) {
-      scaleToFit = true;
-      forcedScale = true;
-    } else if (param_mo.indexOf("file") >= 0) {
-      scaleToFit = false;
-      forcedScale = true;
-    }
-    // operation mode: "errtxt": error message in html, "errimg": error image
-    if (param_mo.indexOf("errtxt") >= 0) {
-      errorMsgHtml = true;
-    } else if (param_mo.indexOf("errimg") >= 0) {
-      errorMsgHtml = false;
-    }
-    // operation mode: "q0" - "q2": interpolation quality
-    if (param_mo.indexOf("q0") >= 0) {
-      scaleQual = 0;
-    } else if (param_mo.indexOf("q1") >= 0) {
-      scaleQual = 1;
-    } else if (param_mo.indexOf("q2") >= 0) {
-      scaleQual = 2;
-    }
-    // operation mode: "lores": try to use scaled image, "hires": unscaled image
-    if (param_mo.indexOf("lores") >= 0) {
-      preScaledFirst = true;
-    } else if (param_mo.indexOf("hires") >= 0) {
-      preScaledFirst = false;
-    }
+			/* check permissions */
+			if (useAuthorization) {
+				// get a list of required roles (empty if no restrictions)
+				List rolesRequired = authOp.rolesForPath(loadPathName, 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();
+					}
+				}
+			}
+
+			// find the file
+			fileset = (ImageFileset) findFile(dlRequest);
+			if (fileset == null) {
+				throw new FileOpException("File " + loadPathName + "("
+						+ dlRequest.getAsInt("pn") + ") not found.");
+			}
+
+			/* calculate expected source image size */
+			ImageSize expectedSourceSize = new ImageSize();
+			if (scaleToFit) {
+				float scale = (1 / Math.min(paramWW, paramWH)) * paramWS;
+				expectedSourceSize.setSize((int) (paramDW * scale),
+						(int) (paramDH * scale));
+			} else {
+				expectedSourceSize.setSize((int) (paramDW * paramWS),
+						(int) (paramDH * paramWS));
+			}
 
-    Utils.dprintln(1, "Parameter values: fn:"+param_fn+" pn:"+param_pn+" dw:"+param_dw+" dh:"+param_dh+" wx:"+param_wx+" wy:"+param_wy+" ww:"+param_ww+" wh:"+param_wh+" ws:"+param_ws+" mo:"+param_mo);
+			ImageFile fileToLoad;
+			/* select a resolution */
+			if (hiresOnly) {
+				// get first element (= highest resolution)
+				fileToLoad = fileset.getBiggest();
+			} else if (loresOnly) {
+				// enforced lores uses next smaller resolution
+				fileToLoad = fileset.getNextSmaller(expectedSourceSize);
+				if (fileToLoad == null) {
+					// this is the smallest we have
+					fileToLoad = fileset.getSmallest();
+				}
+			} else {
+				// autores: use next higher resolution
+				fileToLoad = fileset.getNextBigger(expectedSourceSize);
+				if (fileToLoad == null) {
+					// this is the highest we have
+					fileToLoad = fileset.getBiggest();
+				}
+			}
+			logger.info("Planning to load: " + fileToLoad);
 
-    //"big" try for all file/image actions
-    try {
+			/*
+			 * send the image if its mo=(raw)file
+			 */
+			if (dlRequest.hasOption("mo", "file")
+					|| dlRequest.hasOption("mo", "rawfile")) {
+				if (sendFileAllowed) {
+					String mt = null;
+					if (dlRequest.hasOption("mo", "rawfile")) {
+						mt = "application/octet-stream";
+					}
+					logger.debug("Sending RAW File as is.");
+					ServletOps.sendFile(fileToLoad, mt, response);
+					logger.info("Done in "
+							+ (System.currentTimeMillis() - startTime) + "ms");
+					return;
+				}
+			}
 
-    // DocuImage instance
-    DocuImage docuImage = new JAIDocuImage(util);
-//    DocuImage docuImage = new JIMIDocuImage(util);
-    //DocuImage docuImage = new ImageLoaderDocuImage(util);
+			// check the source image
+			fileToLoad.check();
+			// get the source image type
+			mimeType = fileToLoad.getMimetype();
+			// get the source image size
+			ImageSize imgSize = fileToLoad.getSize();
 
-
-    /**
-     *  find the file to load/send
-     */
+			// decide if the image can be sent as is
+			boolean mimetypeSendable = mimeType.equals("image/jpeg")
+					|| mimeType.equals("image/png")
+					|| mimeType.equals("image/gif");
+			boolean imagoOptions = dlRequest.hasOption("mo", "hmir")
+					|| dlRequest.hasOption("mo", "vmir") || (paramROT != 0)
+					|| (paramRGBM != null) || (paramRGBA != null)
+					|| (paramCONT != 0) || (paramBRGT != 0);
+			boolean imageSendable = mimetypeSendable && !imagoOptions;
 
-    String loadPathName = "";
-    // if there's PathInfo, append
-    if (request.getPathInfo() != null) {
-      loadPathName += request.getPathInfo();
-    }
-    // append fn parameter
-    loadPathName += param_fn;
-    // if it's zoomed, try hires version (to be optimized...)
-    if ((param_ww < 1f) || (param_wh < 1f)) {
-      preScaledFirst = false;
-    }
+			/*
+			 * if not autoRes and image smaller than requested size then send as
+			 * is. if autoRes and image has requested size then send as is. if
+			 * not autoScale and not scaleToFit nor cropToFit then send as is
+			 * (mo=file)
+			 */
+			if (imageSendable
+					&& ((loresOnly && fileToLoad.getSize().isSmallerThan(
+							expectedSourceSize)) || (!(loresOnly || hiresOnly) && fileToLoad
+							.getSize().fitsIn(expectedSourceSize)))) {
+
+				logger.debug("Sending File as is.");
+
+				ServletOps.sendFile(fileToLoad, null, response);
+
+				logger.info("Done in "
+						+ (System.currentTimeMillis() - startTime) + "ms");
+				return;
+			}
 
-    if (useAuthentication) {
-      // check permissions
-      List rolesRequired = authOp.rolesForPath(loadPathName, request);
-      if (rolesRequired != null) {
-        Utils.dprintln(1, "Role required: "+rolesRequired);
-        Utils.dprintln(2, "User: "+request.getRemoteUser());
-        if (! authOp.isRoleAuthorized(rolesRequired, request)) {
-          Utils.dprintln(1, "ERROR: access denied!");
-          if (errorMsgHtml) {
-            servletOp.htmlMessage("ERROR: Unauthorized access!", response);
-          } else {
-            docuImage.sendFile(denyImgFile, response);
-          }
-          return;
-        }
-      }
-    }
+			// set missing dw or dh from aspect ratio
+			float imgAspect = imgSize.getAspect();
+			if (paramDW == 0) {
+				paramDW = (int) Math.round(paramDH * imgAspect);
+			} else if (paramDH == 0) {
+				paramDH = (int) Math.round(paramDW / imgAspect);
+			}
+
+			/*
+			 * prepare resolution for original size
+			 */
+			if (absoluteScale) {
+				// get original resolution from metadata
+				fileset.check();
+				origResX = fileset.getResX();
+				origResY = fileset.getResY();
+				if ((origResX == 0) || (origResY == 0)) {
+					throw new ImageOpException("Missing image DPI information!");
+				}
 
-    // find the file
-    File fileToLoad = fileOp.getFileVariant(baseDirs, loadPathName, param_pn, preScaledFirst);
+				if ((paramDDPIX == 0) || (paramDDPIY == 0)) {
+					throw new ImageOpException(
+							"Missing display DPI information!");
+				}
+			}
 
-    Utils.dprintln(1, "Loading: "+fileToLoad);
+			/* crop and scale the image */
 
-    // get the source image type (if it's known)
-    mimeType = fileOp.mimeForFile(fileToLoad);
+			logger.debug("IMG: " + imgSize.getWidth() + "x"
+					+ imgSize.getHeight());
+			logger.debug("time " + (System.currentTimeMillis() - startTime)
+					+ "ms");
 
-    // if not forced and source is GIF/PNG then send-as-is if not zoomed
-    if((!forcedScale && (mimeType == "image/gif" || mimeType == "image/png")
-        && (param_ww == 1f) && (param_wh == 1f)) || (forcedScale && !scaleToFit)) {
-
-      Utils.dprintln(1, "Sending File as is.");
+			// coordinates and scaling
+			float areaXoff;
+			float areaYoff;
+			float areaWidth;
+			float areaHeight;
+			float scaleX;
+			float scaleY;
+			float scaleXY;
 
-      docuImage.sendFile(fileToLoad, response);
-
-      Utils.dprintln(1, "Done in "+(System.currentTimeMillis()-startTime)+"ms");
-      return;
-    }
-
-    // load file
-    docuImage.loadImage(fileToLoad);
-
-    /**
-     *  crop and scale the image
-     */
-
-    // get size
-    int imgWidth = docuImage.getWidth();
-    int imgHeight = docuImage.getHeight();
-
-    util.dprintln(2, "IMG: "+imgWidth+"x"+imgHeight);
-    util.dprintln(2, "time "+(System.currentTimeMillis()-startTime)+"ms");
+			// coordinates using Java2D
+			// image size in pixels
+			Rectangle2D imgBounds = new Rectangle2D.Float(0, 0, imgSize
+					.getWidth(), imgSize.getHeight());
+			// user window area in [0,1] coordinates
+			Rectangle2D relUserArea = new Rectangle2D.Float(paramWX, paramWY,
+					paramWW, paramWH);
+			// transform from relative [0,1] to image coordinates.
+			AffineTransform imgTrafo = AffineTransform.getScaleInstance(imgSize
+					.getWidth(), imgSize.getHeight());
+			// transform user coordinate area to image coordinate area
+			Rectangle2D userImgArea = imgTrafo.createTransformedShape(
+					relUserArea).getBounds2D();
 
-    // calculate absolute from relative coordinates
-    float areaXoff = param_wx * imgWidth;
-    float areaYoff = param_wy * imgHeight;
-    float areaWidth = param_ww * imgWidth;
-    float areaHeight = param_wh * imgHeight;
-    // calculate scaling factors
-    float scaleX = param_dw / areaWidth * param_ws;
-    float scaleY = param_dh / areaHeight * param_ws;
-    float scaleXY = (scaleX > scaleY) ? scaleY : scaleX;
-
-    util.dprintln(1, "Scale "+scaleXY+"("+scaleX+","+scaleY+") on "+areaXoff+","+areaYoff+" "+areaWidth+"x"+areaHeight);
+			// calculate scaling factors based on inner user area
+			if (scaleToFit) {
+				areaWidth = (float) userImgArea.getWidth();
+				areaHeight = (float) userImgArea.getHeight();
+				scaleX = paramDW / areaWidth * paramWS;
+				scaleY = paramDH / areaHeight * paramWS;
+				scaleXY = (scaleX > scaleY) ? scaleY : scaleX;
+			} else if (absoluteScale) {
+				// absolute scale
+				scaleX = paramDDPIX / origResX;
+				scaleY = paramDDPIY / origResY;
+				// currently only same scale :-(
+				scaleXY = scaleX;
+				areaWidth = paramDW / scaleXY * paramWS;
+				areaHeight = paramDH / scaleXY * paramWS;
+				// reset user area size
+				userImgArea.setRect(userImgArea.getX(), userImgArea.getY(),
+						areaWidth, areaHeight);
+			} else {
+				// crop to fit
+				areaWidth = paramDW * paramWS;
+				areaHeight = paramDH * paramWS;
+				// reset user area size
+				userImgArea.setRect(userImgArea.getX(), userImgArea.getY(),
+						areaWidth, areaHeight);
+				scaleX = 1f;
+				scaleY = 1f;
+				scaleXY = 1f;
+			}
 
-    // fit area to image
-    areaWidth = (areaXoff + areaWidth > imgWidth) ? imgWidth - areaXoff : areaWidth;
-    areaHeight = (areaYoff + areaHeight > imgHeight) ? imgHeight - areaYoff : areaHeight;
-
-    util.dprintln(2, "cropped: "+areaXoff+","+areaYoff+" "+areaWidth+"x"+areaHeight);
+			// enlarge image area for rotations to cover additional pixels
+			Rectangle2D outerUserImgArea = userImgArea;
+			Rectangle2D innerUserImgArea = userImgArea;
+			if (wholeRotArea) {
+				if (paramROT != 0) {
+					try {
+						// rotate user area coordinates around center of user
+						// area
+						AffineTransform rotTrafo = AffineTransform
+								.getRotateInstance(Math.toRadians(paramROT),
+										userImgArea.getCenterX(), userImgArea
+												.getCenterY());
+						// get bounds from rotated end position
+						innerUserImgArea = rotTrafo.createTransformedShape(
+								userImgArea).getBounds2D();
+						// get bounds from back-rotated bounds
+						outerUserImgArea = rotTrafo.createInverse()
+								.createTransformedShape(innerUserImgArea)
+								.getBounds2D();
+					} catch (NoninvertibleTransformException e1) {
+						// this shouldn't happen anyway
+						logger.error(e1);
+					}
+				}
+			}
 
-    // check image parameters
-    if ((areaWidth < 1)||(areaHeight < 1)
-       ||(scaleXY * areaWidth < 2)||(scaleXY * areaHeight < 2)) {
-      Utils.dprintln(1, "ERROR: invalid scale parameter set!");
-      throw new ImageOpException("Invalid scale parameter set!");
-    }
+			logger.debug("Scale " + scaleXY + "(" + scaleX + "," + scaleY
+					+ ") on " + outerUserImgArea);
+
+			// clip area at the image border
+			outerUserImgArea = outerUserImgArea.createIntersection(imgBounds);
 
-    // crop and scale image
-    docuImage.cropAndScale((int)areaXoff, (int)areaYoff, (int)areaWidth, (int)areaHeight,
-                            scaleXY, scaleQual);
-
-    util.dprintln(2, "time "+(System.currentTimeMillis()-startTime)+"ms");
+			// check image parameters sanity
+			if ((outerUserImgArea.getWidth() < 1)
+					|| (outerUserImgArea.getHeight() < 1)
+					|| (scaleXY * outerUserImgArea.getWidth() < 2)
+					|| (scaleXY * outerUserImgArea.getHeight() < 2)) {
+				logger.error("ERROR: invalid scale parameter set!");
+				throw new ImageOpException("Invalid scale parameter set!");
+			}
 
-    /**
-     *  write the resulting image
-     */
+			/*
+			 * submit the image worker job
+			 */
+
+			DigilibWorker job = new DigilibImageWorker(dlConfig, response,
+					mimeType, scaleQual, dlRequest, paramROT, paramCONT,
+					paramBRGT, paramRGBM, paramRGBA, fileToLoad, scaleXY,
+					outerUserImgArea, innerUserImgArea, minSubsample,
+					wholeRotArea, forceJPEG);
 
-    // setup output -- if source is JPG then dest will be JPG else it's PNG
-    if (mimeType != "image/jpeg") {
-      mimeType="image/png";
-    }
+			job.run();
+			if (job.hasError()) {
+				throw new ImageOpException(job.getError().toString());
+			}
+
+			logger.debug("servlet done in "
+					+ (System.currentTimeMillis() - startTime));
+
+			/* error handling */
 
-    // write the image
-    docuImage.writeImage(mimeType, response);
-
-    util.dprintln(1, "Done in "+(System.currentTimeMillis()-startTime)+"ms");
-
-    /**
-     *  error handling
-     */
+		} // end of "big" try
+		catch (IOException e) {
+			logger.error("ERROR: File IO Error: " + e);
+			digilibError(errorMsgHtml, ERROR_FILE,
+					"ERROR: File IO Error: " + e, response);
+		} catch (AuthOpException e) {
+			logger.error("ERROR: Authorization error: " + e);
+			digilibError(errorMsgHtml, ERROR_AUTH,
+					"ERROR: Authorization error: " + e, response);
+		} catch (ImageOpException e) {
+			logger.error("ERROR: Image Error: " + e);
+			digilibError(errorMsgHtml, ERROR_IMAGE,
+					"ERROR: Image Operation Error: " + e, response);
+		} catch (RuntimeException e) {
+			// JAI likes to throw RuntimeExceptions ;-(
+			logger.error("ERROR: Other Image Error: " + e);
+			digilibError(errorMsgHtml, ERROR_IMAGE,
+					"ERROR: Other Image Operation Error: " + e, response);
+		}
+	}
 
-    }//"big" try
-    catch (FileOpException e) {
-      util.dprintln(1, "ERROR: File IO Error: "+e);
-      try {
-        if (errorMsgHtml) {
-          servletOp.htmlMessage("ERROR: File IO Error: "+e, response);
-        } else {
-          globalImage.sendFile(errorImgFile, response);
-        }
-      } catch (FileOpException ex) {} // so we don't get a loop
-      return;
-    }
-    catch (AuthOpException e) {
-      Utils.dprintln(1, "ERROR: Authorization error: "+e);
-      try {
-        if (errorMsgHtml) {
-          servletOp.htmlMessage("ERROR: Authorization error: "+e, response);
-        } else {
-          globalImage.sendFile(errorImgFile, response);
-        }
-      } catch (FileOpException ex) {} // so we don't get a loop
-      return;
-    }
-    catch (ImageOpException e) {
-      Utils.dprintln(1, "ERROR: Image Error: "+e);
-      try {
-        if (errorMsgHtml) {
-          servletOp.htmlMessage("ERROR: Image Operation Error: "+e, response);
-        } else {
-          globalImage.sendFile(errorImgFile, response);
-        }
-      } catch (FileOpException ex) {} // so we don't get a loop
-      return;
-    }
+	/**
+	 * Returns the DocuDirent corresponding to the DigilibRequest.
+	 * 
+	 * @param dlRequest
+	 * @return
+	 */
+	public DigiDirent findFile(DigilibRequest dlRequest) {
+		// find the file(set)
+		DigiDirent f = dirCache.getFile(dlRequest.getFilePath(), dlRequest
+				.getAsInt("pn"), FileOps.CLASS_IMAGE);
+		return f;
+	}
 
-  }
+	/**
+	 * Sends an error to the client as text or image.
+	 * 
+	 * @param asHTML
+	 * @param type
+	 * @param msg
+	 * @param response
+	 */
+	public void digilibError(boolean asHTML, int type, String msg,
+			HttpServletResponse response) {
+		try {
+			File img = null;
+			if (type == ERROR_AUTH) {
+				if (msg == null) {
+					msg = "ERROR: Unauthorized access!";
+				}
+				img = denyImgFile;
+			} else {
+				if (msg == null) {
+					msg = "ERROR: Other image error!";
+				}
+				img = this.errorImgFile;
+			}
+			if (asHTML && (img != null)) {
+				ServletOps.htmlMessage(msg, response);
+			} else {
+				ServletOps.sendFile(img, null, response);
+			}
+		} catch (IOException e) {
+			logger.error("Error sending error!", e);
+		}
 
-}//Scaler class
+	}
+
+} //Scaler class