changeset 866:5431156f7b9d stream

Merge from jquery branch 7ebdc106a61ace189d41df7919f8c667d10584fe
author robcast
date Thu, 10 Mar 2011 10:57:08 +0100
parents ace973a106b5 (diff) 7ebdc106a61a (current diff)
children 987cfe401970
files
diffstat 7 files changed, 279 insertions(+), 212 deletions(-) [+]
line wrap: on
line diff
--- a/client/digitallibrary/jquery/digilib.html	Wed Mar 09 00:39:17 2011 +0100
+++ b/client/digitallibrary/jquery/digilib.html	Thu Mar 10 10:57:08 2011 +0100
@@ -23,7 +23,8 @@
         <script type="text/javascript">
             $(document).ready(function(){
                 var opts = {
-                    interactionMode : 'fullscreen'
+                    interactionMode : 'fullscreen',
+                    showRegionNumbers : true
                     };
                 var $div = $('div.digilib');
                 $div.digilib(opts);
--- a/servlet/src/digilib/image/DocuImage.java	Wed Mar 09 00:39:17 2011 +0100
+++ b/servlet/src/digilib/image/DocuImage.java	Thu Mar 10 10:57:08 2011 +0100
@@ -205,14 +205,22 @@
 
 	/** Operations for colorOps.
 	 * 
+     * GRAYSCALE: cast color image to grayscale
+     * NTSC_GRAY: convert color image to grayscale using NTSC formula
+     * INVERT: invert colors (every channel separately)
+     * MAP_GRAY_BGR: false color image from grayscale (0: blue, 128: green, 255: red)
 	 *
 	 */
-	public enum ColorOp {GRAYSCALE, INVERT};
+	public enum ColorOp {GRAYSCALE, NTSC_GRAY, INVERT, MAP_GRAY_BGR};
 
 	/** Changes the colors of the current image.
 	 * 
-	 * Changes the colors of the current image. Operations are e.g.
-	 * reduction to grayscale or false-color palettes.
+	 * Changes the colors of the current image. Operations are instances of ColorOp:
+	 * 
+	 * GRAYSCALE: cast color image to grayscale
+     * NTSC_GRAY: convert color image to grayscale using NTSC formula
+	 * INVERT: invert colors (every channel separately)
+	 * MAP_GRAY_BGR: false color image from grayscale (0: blue, 128: green, 255: red)
 	 * 
 	 * @throws ImageOpException
 	 */
--- a/servlet/src/digilib/image/DocuImageImpl.java	Wed Mar 09 00:39:17 2011 +0100
+++ b/servlet/src/digilib/image/DocuImageImpl.java	Thu Mar 10 10:57:08 2011 +0100
@@ -53,7 +53,7 @@
 	protected int quality = 0;
 	
 	/** epsilon for float comparisons. */
-	public final double epsilon = 1e-5;
+	public static final double epsilon = 1e-5;
 
 	/** image size */
     protected ImageSize imgSize = null;
--- a/servlet/src/digilib/image/ImageJobDescription.java	Wed Mar 09 00:39:17 2011 +0100
+++ b/servlet/src/digilib/image/ImageJobDescription.java	Thu Mar 10 10:57:08 2011 +0100
@@ -432,6 +432,9 @@
 
 	public ColorOp getColOp() {
 		String op = getAsString("colop");
+		if (op == null || op.length() == 0) {
+			return null;
+		}
 		try {
 			return ColorOp.valueOf(op.toUpperCase());
 		} catch (Exception e) {
--- a/servlet/src/digilib/image/ImageLoaderDocuImage.java	Wed Mar 09 00:39:17 2011 +0100
+++ b/servlet/src/digilib/image/ImageLoaderDocuImage.java	Thu Mar 10 10:57:08 2011 +0100
@@ -26,11 +26,13 @@
 import java.awt.geom.AffineTransform;
 import java.awt.geom.Rectangle2D;
 import java.awt.image.AffineTransformOp;
+import java.awt.image.BandCombineOp;
 import java.awt.image.BufferedImage;
 import java.awt.image.ByteLookupTable;
 import java.awt.image.ColorConvertOp;
 import java.awt.image.ColorModel;
 import java.awt.image.ConvolveOp;
+import java.awt.image.IndexColorModel;
 import java.awt.image.Kernel;
 import java.awt.image.LookupOp;
 import java.awt.image.LookupTable;
@@ -74,22 +76,67 @@
             new Kernel(3, 3, new float[] {1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f})
 	};
 
-	/** lookup table for inverting images (byte) */
-	protected static LookupTable invertByteTable;
-    protected static LookupTable invertRGBByteTable;
-	
+	/* lookup tables for inverting images (byte) */
+	protected static LookupTable invertSingleByteTable;
+    protected static LookupTable invertRgbaByteTable;
+    protected static boolean needsInvertRgba = false;
+	/* RescaleOp for contrast/brightness operation */
+    protected static boolean needsRescaleRgba = false;
+    /* lookup table for false-color */
+    protected static LookupTable mapBgrByteTable;
+    protected static boolean needsMapBgr = false;
+    
 	static {
+	    /*
+	     * create static lookup tables
+	     */
 		byte[] invertByte = new byte[256];
 		byte[] orderedByte = new byte[256];
+		byte[] nullByte = new byte[256];
+        byte[] mapR = new byte[256];
+        byte[] mapG = new byte[256];
+        byte[] mapB = new byte[256];
 		for (int i = 0; i < 256; ++i) {
+		    // counting down
 			invertByte[i] = (byte) (256 - i);
+			// counting up
 			orderedByte[i] = (byte) i;
+			// constant 0
+			nullByte[i] = 0;
+			// three overlapping slopes
+			if (i < 64) {
+			    mapR[i] = 0;
+			    mapG[i] = (byte) (4 * i);
+			    mapB[i] = (byte) 255;
+			} else if (i >= 64 && i < 192) {
+                mapR[i] = (byte) (2 * (i - 64));
+                mapG[i] = (byte) 255;
+                mapB[i] = (byte) (255 - 2 * (i - 64));
+			} else {
+                mapR[i] = (byte) 255;
+                mapG[i] = (byte) (255 - (4 * (i - 192)));
+                mapB[i] = 0;
+			}
 		}
-		// works for JPEG in q2
-		invertRGBByteTable = new ByteLookupTable(0, new byte[][] {
-				orderedByte, invertByte, invertByte});
-		// should work for all color models
-		invertByteTable = new ByteLookupTable(0, invertByte);
+		// should(!) work for all color models
+		invertSingleByteTable = new ByteLookupTable(0, invertByte);
+		// but doesn't work with alpha channel on all platforms
+		String ver = System.getProperty("java.version");
+		String os =  System.getProperty("os.name");
+		logger.debug("os="+os+" ver="+ver);
+		if (os.startsWith("Linux") && ver.startsWith("1.6")) {
+			// GRAB(WTF?) works in Linux JDK1.6 with transparency
+			invertRgbaByteTable = new ByteLookupTable(0, new byte[][] {
+					invertByte, invertByte, orderedByte, invertByte});
+			needsInvertRgba = true;
+			needsRescaleRgba = true;
+			needsMapBgr = true;
+		} else {
+			invertRgbaByteTable = invertSingleByteTable;
+		}
+		// this hopefully works for all
+        mapBgrByteTable = new ByteLookupTable(0, new byte[][] {
+                mapR, mapG, mapB});
 	}
 	
 	/** the size of the current image */
@@ -268,6 +315,13 @@
 			logger.debug("loading..");
 			img = reader.read(0, readParam);
 			logger.debug("loaded");
+			/* downconversion of highcolor images seems not to work
+	        if (img.getColorModel().getComponentSize(0) > 8) {
+	            logger.debug("converting to 8bit");
+	            BufferedImage dest = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
+	            dest.createGraphics().drawImage(img, null, 0, 0);
+	            img = dest;
+	        } */
 		} catch (IOException e) {
 			throw new FileOpException("Unable to load File!");
 		} finally {
@@ -289,21 +343,16 @@
 			if (mt == "image/jpeg") {
 				/*
 				 * JPEG doesn't do transparency so we have to convert any RGBA
-				 * image to RGB :-( *Java2D BUG*
+				 * image to RGB or we the client will think its CMYK :-( *Java2D BUG*
 				 */
 				if (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;
+                    BufferedImage rgbImg = new BufferedImage(img.getWidth(),
+                            img.getHeight(), BufferedImage.TYPE_INT_RGB);
+					rgbImg.createGraphics().drawImage(img, null, 0, 0);
+					img = rgbImg;
 				}
-				writer = (ImageWriter) ImageIO.getImageWritersByFormatName(
-						"jpeg").next();
+				writer = ImageIO.getImageWritersByFormatName("jpeg").next();
 				if (writer == null) {
 					throw new ImageOpException("Unable to get JPEG writer");
 				}
@@ -311,25 +360,20 @@
 				if (quality > 1) {
 					// change JPEG compression quality
 					param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
-					//logger.debug("JPEG qual before: "
-					//		+ Float.toString(param.getCompressionQuality()));
 					param.setCompressionQuality(0.9f);
-					//logger.debug("JPEG qual now: "
-					//		+ Float.toString(param.getCompressionQuality()));
 				}
 				writer.setOutput(imgout);
 				// render output
-				logger.debug("writing");
+				logger.debug("writing JPEG");
 				writer.write(null, new IIOImage(img, null, null), param);
 			} else if (mt == "image/png") {
 				// render output
-				writer = (ImageWriter) ImageIO.getImageWritersByFormatName(
-						"png").next();
+				writer = ImageIO.getImageWritersByFormatName("png").next();
 				if (writer == null) {
 					throw new ImageOpException("Unable to get PNG writer");
 				}
 				writer.setOutput(imgout);
-				logger.debug("writing");
+				logger.debug("writing PNG");
 				writer.write(img);
 			} else {
 				// unknown mime type
@@ -343,36 +387,20 @@
 		// TODO: should we: finally { writer.dispose(); }
 	}
 
-	public void scale(double scale, double scaleY) throws ImageOpException {
-		logger.debug("scale: " + scale);
-		/* for downscaling in high quality the image is blurred first */
-		if ((scale <= 0.5) && (quality > 1)) {
-			int bl = (int) Math.floor(1 / scale);
-			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();
-		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);
-		if (scaledImg == null) {
-			throw new ImageOpException("Unable to scale");
-		}
-		// DEBUG
-		logger.debug("destination image type " + scaledImg.getType());
-		logger.debug("SCALE: " + scale + " ->" + scaledImg.getWidth() + "x"
-				+ scaledImg.getHeight());
-		img = scaledImg;
-	}
+    public void scale(double scaleX, double scaleY) throws ImageOpException {
+        logger.debug("scale: " + scaleX);
+        /* for downscaling in high quality the image is blurred first */
+        if ((scaleX <= 0.5) && (quality > 1)) {
+            int bl = (int) Math.floor(1 / scaleX);
+            blur(bl);
+        }
+        /* then scaled */
+        AffineTransformOp scaleOp = new AffineTransformOp(
+                AffineTransform.getScaleInstance(scaleX, scaleY), renderHint);
+        img = scaleOp.filter(img, null);
+        logger.debug("scaled to " + img.getWidth() + "x" + img.getHeight()
+                + " img=" + img);
+    }
 
 	public void blur(int radius) throws ImageOpException {
 		logger.debug("blur: " + radius);
@@ -380,6 +408,7 @@
 		int klen = Math.max(radius, 2);
 		Kernel blur = null;
 		if (klen < convolutionKernels.length) {
+		    // use precalculated Kernel
             blur = convolutionKernels[klen];
 		} else {
             // calculate our own kernel
@@ -395,166 +424,52 @@
 		// blur with convolve operation
 		ConvolveOp blurOp = new ConvolveOp(blur, ConvolveOp.EDGE_NO_OP,
 				renderHint);
-		BufferedImage blurredImg = null;
-		// blur needs explicit destination image type for color *Java2D BUG*
+		BufferedImage dest = null;
+        // blur needs explicit destination image type for 3BYTE_BGR *Java2D BUG*
 		if (img.getType() == BufferedImage.TYPE_3BYTE_BGR) {
-			logger.debug("blur: fixing destination image type");
-			blurredImg = new BufferedImage(img.getWidth(), img.getHeight(), img
-					.getType());
+		    logger.debug("blur: fixing destination image type");
+		    dest = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
 		}
-		blurredImg = blurOp.filter(img, blurredImg);
-		img = blurredImg;
+		img = blurOp.filter(img, dest);
+		logger.debug("blurred: "+img);
 	}
 
 	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);
-		if (croppedImg == null) {
-			throw new ImageOpException("Unable to crop");
-		}
-		logger.debug("CROP:" + croppedImg.getWidth() + "x"
-				+ croppedImg.getHeight());
-		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);
-	}
-
-	/**
-	 * Ensures that the array f is in the right order to map the images RGB
-	 * components. (not sure what happens otherwise)
-	 */
-	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;
-	}
-
-	/*
-	 * (non-Javadoc)
-	 * 
-	 * @see
-	 * digilib.image.DocuImageImpl#colorOp(digilib.image.DocuImage.ColorOps)
-	 */
-	public void colorOp(ColorOp op) throws ImageOpException {
-		if (op == ColorOp.GRAYSCALE) {
-			// convert image to grayscale
-			logger.debug("Color op: grayscaling");
-			ColorConvertOp colop = new ColorConvertOp(
-					ColorSpace.getInstance(ColorSpace.CS_GRAY), renderHint);
-			img = colop.filter(img, null);
-		} else if (op == ColorOp.INVERT) {
-			// invert colors i.e. invert every channel
-			logger.debug("Color op: inverting");
-			// TODO: is this enough for all image types?
-			LookupOp colop = new LookupOp(invertByteTable, renderHint);
-			ColorModel cm = img.getColorModel();
-			logger.debug("colop: image="+img+" colormodel="+cm);
-			img = colop.filter(img, null);
-		}
-
+		img = img.getSubimage(x_off, y_off, width, height);
+		logger.debug("CROP:" + img.getWidth() + "x"
+				+ img.getHeight());
 	}
 
 	public void rotate(double angle) throws ImageOpException {
+        logger.debug("rotate: " + angle);
 		// 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
+		// center of rotation is center of image
+        double w = img.getWidth();
+        double h = img.getHeight();
 		double x = (w / 2);
 		double y = (h / 2);
-		trafo.rotate(rangle, x, y);
-		// try rotation to see how far we're out of bounds
+        AffineTransform trafo = AffineTransform.getRotateInstance(rangle, x, y);
 		AffineTransformOp rotOp = new AffineTransformOp(trafo, renderHint);
+        // rotate bounds to see how much of the image would be off screen
 		Rectangle2D rotbounds = rotOp.getBounds2D(img);
 		double xoff = rotbounds.getX();
 		double yoff = rotbounds.getY();
-		// move image back in line
-		trafo.preConcatenate(AffineTransform.getTranslateInstance(-xoff, -yoff));
+		if (Math.abs(xoff) > epsilon || Math.abs(yoff) > epsilon) {
+		    // move image back on screen
+		    logger.debug("move rotation: xoff="+xoff+" yoff="+yoff);
+		    trafo.preConcatenate(AffineTransform.getTranslateInstance(-xoff, -yoff));
+	        rotOp = new AffineTransformOp(trafo, renderHint);
+		}
 		// transform image
-		rotOp = new AffineTransformOp(trafo, renderHint);
-		BufferedImage rotImg = rotOp.filter(img, null);
-		// calculate new bounding box
-		// Rectangle2D bounds = rotOp.getBounds2D(img);
-		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));
-		 */
+		img = rotOp.filter(img, null);
+		logger.debug("rotated: "+img);
 	}
 
 	public void mirror(double angle) throws ImageOpException {
+        logger.debug("mirror: " + angle);
 		// setup mirror
 		double mx = 1;
 		double my = 1;
@@ -562,26 +477,166 @@
 		double ty = 0;
 		if (Math.abs(angle - 0) < epsilon) { // 0 degree
 			mx = -1;
-			tx = getWidth();
+			tx = img.getWidth();
 		} else if (Math.abs(angle - 90) < epsilon) { // 90 degree
 			my = -1;
-			ty = getHeight();
+			ty = img.getHeight();
 		} else if (Math.abs(angle - 180) < epsilon) { // 180 degree
 			mx = -1;
-			tx = getWidth();
+			tx = img.getWidth();
 		} else if (Math.abs(angle - 270) < epsilon) { // 270 degree
 			my = -1;
-			ty = getHeight();
+			ty = img.getHeight();
 		} else if (Math.abs(angle - 360) < epsilon) { // 360 degree
 			mx = -1;
-			tx = getWidth();
+			tx = img.getWidth();
+		} else {
+		    logger.error("invalid mirror angle "+angle);
+		    return;
 		}
 		AffineTransformOp mirOp = new AffineTransformOp(new AffineTransform(mx,
 				0, 0, my, tx, ty), renderHint);
-		BufferedImage mirImg = mirOp.filter(img, null);
-		img = mirImg;
+		img = mirOp.filter(img, null);
 	}
 
+    public void enhance(float mult, float add) throws ImageOpException {
+        RescaleOp op = null;
+        logger.debug("enhance: img=" + img);
+        if (needsRescaleRgba) {
+            /*
+             * 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).
+             * 
+             * 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;
+            }
+            op = new RescaleOp(dm, da, null);
+        } else {
+            op = new RescaleOp(mult, add, renderHint);
+        }
+        op.filter(img, img);
+    }
+
+    public void enhanceRGB(float[] rgbm, float[] rgba) throws ImageOpException {
+        logger.debug("enhanceRGB: rgbm="+rgbm+" rgba="+rgba);
+        /*
+         * 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.error("enhanceRGB: unknown number of color bands or coefficients ("
+                            + ncol + ")");
+            return;
+        }
+        if (img.getColorModel().hasAlpha()) {
+            // add constant for alpha
+            rgbm = new float[] {rgbm[0], rgbm[1], rgbm[2], 1};
+            rgba = new float[] {rgba[0], rgba[1], rgba[2], 0};
+        }
+        RescaleOp scaleOp = new RescaleOp(rgbm, rgba, renderHint);
+        scaleOp.filter(img, img);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * digilib.image.DocuImageImpl#colorOp(digilib.image.DocuImage.ColorOps)
+     */
+    public void colorOp(ColorOp colop) throws ImageOpException {
+        if (colop == ColorOp.GRAYSCALE) {
+            /*
+             * convert image to grayscale
+             */
+            logger.debug("Color op: grayscaling");
+            ColorModel cm = img.getColorModel();
+            if (cm.getNumColorComponents() < 3) {
+                // grayscale already
+                logger.debug("Color op: not grayscaling");
+                return;
+            }
+            ColorConvertOp op = new ColorConvertOp(
+                    ColorSpace.getInstance(ColorSpace.CS_GRAY), renderHint);
+            // let filter create new image
+            img = op.filter(img, null);
+        } else if (colop == ColorOp.NTSC_GRAY) {
+            /*
+             * convert image to grayscale NTSC-style: luminance = 0.2989*red +
+             * 0.5870*green + 0.1140*blue
+             */
+            logger.debug("Color op: NTSC gray");
+            logger.debug("img="+img);
+            ColorModel cm = img.getColorModel();
+            if (cm.getNumColorComponents() < 3 || cm instanceof IndexColorModel) {
+                // grayscale already or not possible
+                logger.debug("Color op: unable to NTSC gray");
+                return;
+            }
+            float[][] combineFn = new float[1][4];
+            combineFn[0] = new float[] { 0.299f, 0.587f, 0.114f, 0f };
+            BandCombineOp op = new BandCombineOp(combineFn, renderHint);
+            // BandCombineOp only works on Rasters so we create a
+            // new image and use its Raster
+            BufferedImage dest = new BufferedImage(img.getWidth(),
+                    img.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
+            op.filter(img.getRaster(), dest.getRaster());
+            img = dest;
+        } else if (colop == ColorOp.INVERT) {
+            /*
+             * invert colors i.e. invert every channel
+             */
+            logger.debug("Color op: inverting");
+            LookupTable invtbl = null;
+            ColorModel cm = img.getColorModel();
+            if (cm instanceof IndexColorModel) {
+                // invert not possible
+                // TODO: should we convert?
+                logger.debug("Color op: unable to invert");
+                return;
+            }
+            if (needsInvertRgba && cm.hasAlpha()) {
+                // fix for some cases
+                invtbl = invertRgbaByteTable;
+            } else {
+                invtbl = invertSingleByteTable;
+            }
+            LookupOp op = new LookupOp(invtbl, renderHint);
+            logger.debug("colop: image=" + img);
+            op.filter(img, img);
+        } else if (colop == ColorOp.MAP_GRAY_BGR) {
+            /*
+             * false color image from grayscale (0: blue, 128: green, 255: red)
+             */
+            logger.debug("Color op: map_gray_bgr");
+            // convert to grayscale
+            ColorConvertOp grayOp = new ColorConvertOp(
+                    ColorSpace.getInstance(ColorSpace.CS_GRAY), renderHint);
+            // create new 3-channel image
+            int destType = BufferedImage.TYPE_INT_RGB;
+            if (needsMapBgr) {
+                // special case for broken Java2Ds
+                destType = BufferedImage.TYPE_3BYTE_BGR;
+            }
+            BufferedImage dest = new BufferedImage(img.getWidth(),
+                    img.getHeight(), destType);
+            img = grayOp.filter(img, dest);
+            logger.debug("map_gray: image=" + img);
+            // convert to false color
+            LookupOp mapOp = new LookupOp(mapBgrByteTable, renderHint);
+            mapOp.filter(img, img);
+            logger.debug("mapped image=" + img);
+        }
+    }
+
 	public void dispose() {
 	    // is this necessary?
 		img = null;
--- a/servlet/src/digilib/servlet/Initialiser.java	Wed Mar 09 00:39:17 2011 +0100
+++ b/servlet/src/digilib/servlet/Initialiser.java	Thu Mar 10 10:57:08 2011 +0100
@@ -137,17 +137,17 @@
 				// digilib worker threads
 				int nt = dlConfig.getAsInt("worker-threads");
                 int mt = dlConfig.getAsInt("max-waiting-threads");
-				imageEx = new DigilibJobCenter<DocuImage>(nt, mt, true, "servlet.worker.imageexecutor");
+				imageEx = new DigilibJobCenter<DocuImage>(nt, mt, false, "servlet.worker.imageexecutor");
                 dlConfig.setValue("servlet.worker.imageexecutor", imageEx);				
 				// PDF worker threads
 				int pnt = dlConfig.getAsInt("pdf-worker-threads");
                 int pmt = dlConfig.getAsInt("pdf-max-waiting-threads");
-				pdfEx = new DigilibJobCenter<OutputStream>(pnt, pmt, true, "servlet.worker.pdfexecutor");
+				pdfEx = new DigilibJobCenter<OutputStream>(pnt, pmt, false, "servlet.worker.pdfexecutor");
                 dlConfig.setValue("servlet.worker.pdfexecutor", pdfEx);				
 				// PDF image worker threads
 				int pint = dlConfig.getAsInt("pdf-image-worker-threads");
                 int pimt = dlConfig.getAsInt("pdf-image-max-waiting-threads");
-				pdfImageEx = new DigilibJobCenter<DocuImage>(pint, pimt, true, "servlet.worker.pdfimageexecutor");
+				pdfImageEx = new DigilibJobCenter<DocuImage>(pint, pimt, false, "servlet.worker.pdfimageexecutor");
                 dlConfig.setValue("servlet.worker.pdfimageexecutor", pdfImageEx);				
 				// set as the servlets main config
 				context.setAttribute("digilib.servlet.configuration", dlConfig);
--- a/servlet/src/digilib/servlet/Scaler.java	Wed Mar 09 00:39:17 2011 +0100
+++ b/servlet/src/digilib/servlet/Scaler.java	Thu Mar 10 10:57:08 2011 +0100
@@ -31,7 +31,7 @@
     private static final long serialVersionUID = 5289386646192471549L;
 
     /** digilib servlet version (for all components) */
-    public static final String version = "1.9.1a16";
+    public static final String version = "1.9.1a24";
 
     /** servlet error codes */
     public static enum Error {UNKNOWN, AUTH, FILE, IMAGE};