changeset 1453:e7d94cfbec0b release-2.3

Merge from default branch 9429bb9c3a4239e133b878a464d802bc8984ae60
author robcast
date Fri, 13 Nov 2015 13:09:08 +0100
parents bb8296934cb2 (current diff) 9429bb9c3a42 (diff)
children 02252febba09
files webapp/pom.xml webapp/src/main/webapp/jquery/jquery.digilib.js
diffstat 18 files changed, 765 insertions(+), 259 deletions(-) [+]
line wrap: on
line diff
--- a/README.md	Tue Nov 10 14:41:27 2015 +0100
+++ b/README.md	Fri Nov 13 13:09:08 2015 +0100
@@ -18,6 +18,8 @@
 * `digilib` facilitates cooperation of scholars over the internet and
   novel uses of source material by image annotations and stable references that
   can be embedded in URLs.
+* `digilib` facilitates federation of image servers through a standards compliant
+  [IIIF](http://iiif.io) image API.
 * `digilib` is Open Source Software under the Lesser General Public License,
   jointly developed by the
   [Max-Planck-Institute for the History of Science](http://www.mpiwg-berlin.mpg.de),
--- a/common/src/main/java/digilib/conf/DigilibConfiguration.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/common/src/main/java/digilib/conf/DigilibConfiguration.java	Fri Nov 13 13:09:08 2015 +0100
@@ -57,7 +57,7 @@
 
     /** digilib version */
     public static String getClassVersion() {
-        return "2.3.4";
+        return "2.3.6a";
     }
 
     /* non-static getVersion for Java inheritance */
@@ -94,6 +94,8 @@
         newParameter("default-errmsg-type", "image", null, 'f');
         // prefix for IIIF image API paths (used by DigilibRequest)
         newParameter("iiif-prefix", "IIIF", null, 'f');
+        // IIIF Image API version to support (mostly relevant for info.json)
+        newParameter("iiif-api-version", "2.0", null, 'f');        
         // character to use as slash-replacement in IIIF identifier part
         newParameter("iiif-slash-replacement", "!", null, 'f');        
     }
--- a/common/src/main/java/digilib/conf/DigilibRequest.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/common/src/main/java/digilib/conf/DigilibRequest.java	Fri Nov 13 13:09:08 2015 +0100
@@ -278,11 +278,13 @@
      * 
      * path should be non-URL-decoded and have no leading slash.
      * 
+     * URI template:
+     * {scheme}://{server}{/prefix}/{identifier}/{region}/{size}/{rotation}/{quality}.{format}
+     * 
      * @param path
      *            String with IIIF Image API path.
      * 
-     * @see <a href="http://www-sul.stanford.edu/iiif/image-api/1.1/">IIIF Image
-     *      API</a>
+     * @see <a href="http://iiif.io/api/image/2.0/">IIIF Image API</a>
      */
     public boolean setWithIiifPath(String path) {
         if (path == null) {
@@ -315,7 +317,7 @@
             }
         }
         /*
-         * second parameter FN (encoded)
+         * second parameter identifier (encoded)
          */
         if (query.hasMoreTokens()) {
             token = getNextDecodedToken(query);
@@ -404,11 +406,16 @@
 
 	/**
 	 * Populate a request from IIIF image API parameters.
-	 * 
-	 * {scheme}://{server}{/prefix}/{identifier}/{region}/{size}/{rotation}/{quality}{.format}
+	 *
+	 * @see <a href="http://iiif.io/api/image/2.0/">IIIF Image API</a>
 	 * 
-	 * @see <a href="http://www-sul.stanford.edu/iiif/image-api/1.1/">IIIF Image
-	 *      API</a>
+	 * @param identifier
+	 * @param region
+	 * @param size
+	 * @param rotation
+	 * @param quality
+	 * @param format
+	 * @return
 	 */
 	public boolean setWithIiifParams(String identifier, String region, String size, 
 			String rotation, String quality, String format) {
@@ -449,7 +456,7 @@
                 options.setOption("info");
                 return true;
             } else if (region.equals("full")) {
-                // full region -- default
+                // full image -- default
             } else if (region.startsWith("pct:")) {
                 // pct:x,y,w,h -- region in % of original image
                 String[] parms = region.substring(4).split(",");
@@ -483,8 +490,8 @@
                 }
             }
         } else {
-            // region omitted -- assume info request
-            options.setOption("info");
+            // region omitted -- redirect to info request
+            options.setOption("redirect-info");
             return true;
         }
         
@@ -493,11 +500,16 @@
          */
         if (size != null) {
             if (size.equals("full")) {
-                // full -- size of original
+                /*
+                 * full -- size of original
+                 */
                 options.setOption("ascale");
                 setValue("scale", 1f);
+                
             } else if (size.startsWith("pct:")) {
-                // pct:n -- n% size of original
+                /*
+                 * pct:n -- n% size of original
+                 */
                 try {
                     float pct = Float.parseFloat(size.substring(4));
                     options.setOption("ascale");
@@ -507,17 +519,20 @@
                     logger.error(errorMessage+e);
                     return false;
                 }
+                
             } else {
-                // w,h -- pixel size
+                /*
+                 * w,h -- pixel size
+                 */
                 try {
                     String[] parms = size.split(",", 2);
                     if (parms[0].length() > 0) {
                         // width param
                         if (parms[0].startsWith("!")) {
-                            // width (in digilib-like bounding box)
+                            // !w,h width (in digilib-like bounding box)
                             setValueFromString("dw", parms[0].substring(1));
                         } else if (parms[1].length() == 0) {
-                            // width only
+                            // w, width only
                             setValueFromString("dw", parms[0]);
                         } else {
                             // w,h -- according to spec, we should distort the image to match ;-(
@@ -546,6 +561,11 @@
          * parameter rotation
          */
         if (rotation != null) {
+            if (rotation.startsWith("!")) {
+                // !n -- mirror and rotate
+                options.setOption("hmir");
+                rotation = rotation.substring(1);
+            }
             try {
                 float rot = Float.parseFloat(rotation);
                 setValue("rot", rot);
@@ -561,9 +581,9 @@
          */
         if (quality != null) {
             // quality param
-            if (quality.equals("native") || quality.equals("color")) {
-                // native is default anyway
-            } else if (quality.equals("grey")) {
+            if (quality.equals("default") || quality.equals("native") || quality.equals("color")) {
+                // color is default anyway
+            } else if (quality.equals("gray") || quality.equals("grey")) {
                 setValueFromString("colop", "grayscale");
             } else {
                 errorMessage = "Invalid quality parameter in IIIF path!";
--- a/common/src/main/java/digilib/image/ImageJobDescription.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/common/src/main/java/digilib/image/ImageJobDescription.java	Fri Nov 13 13:09:08 2015 +0100
@@ -1,5 +1,32 @@
 package digilib.image;
 
+/*
+ * #%L
+ * A class for storing the set of parameters necessary for scaling images with an ImageWorker.
+ * 
+ * Digital Image Library servlet components
+ * 
+ * %%
+ * Copyright (C) 2002 - 2015 MPIWG Berlin
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as 
+ * published by the Free Software Foundation, either version 3 of the 
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Lesser Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Lesser Public 
+ * License along with this program.  If not, see
+ * <http://www.gnu.org/licenses/lgpl-3.0.html>.
+ * #L%
+ * Author: Robert Casties (robcast@users.sourceforge.de),
+ *   Christopher Mielack (cmielack@mpiwg-berlin.mpg.de)
+ */
+
 import java.awt.geom.Rectangle2D;
 import java.io.IOException;
 
@@ -56,6 +83,8 @@
     protected Float paramWY = null;
     protected Float paramWW = null;
     protected Float paramWH = null;
+    protected float[] paramRGBM = null;
+    protected float[] paramRGBA = null;
     protected DocuDirCache dirCache = null;
 	protected ImageSize hiresSize = null;
 	protected ImageSize imgSize = null;
@@ -67,6 +96,7 @@
      */
     public ImageJobDescription(DigilibConfiguration dlcfg) {
         super(30);
+        initParams();
         dlConfig = dlcfg;
         dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
     }
@@ -138,13 +168,15 @@
      * @param dlReq
      * @param dlcfg
      * @return
+     * @throws ImageOpException 
+     * @throws IOException 
      */
-    public static ImageJobDescription getInstance(DigilibRequest dlReq, DigilibConfiguration dlcfg) {
+    public static ImageJobDescription getInstance(DigilibRequest dlReq, DigilibConfiguration dlcfg) throws IOException, ImageOpException {
         ImageJobDescription newMap = new ImageJobDescription(dlcfg);
-        newMap.initParams();
         // add all params to this map
         newMap.params.putAll(dlReq.getParams());
         newMap.initOptions();
+        newMap.prepareScaleParams();
         // add ImageJobDescription back into DigilibRequest
         dlReq.setJobDescription(newMap);
         return newMap;
@@ -157,13 +189,15 @@
      * @param pm
      * @param dlcfg
      * @return
+     * @throws ImageOpException 
+     * @throws IOException 
      */
-    public static ImageJobDescription getInstance(ParameterMap pm, DigilibConfiguration dlcfg) {
+    public static ImageJobDescription getInstance(ParameterMap pm, DigilibConfiguration dlcfg) throws IOException, ImageOpException {
         ImageJobDescription newMap = new ImageJobDescription(dlcfg);
-        newMap.initParams();
         // add all params to this map
         newMap.params.putAll(pm.getParams());
         newMap.initOptions();
+        newMap.prepareScaleParams();
         return newMap;
     }
 
@@ -171,8 +205,8 @@
     /**
      * Prepare image scaling factors and coordinates.
      * 
+     * Should be called by getInstance().
      * Uses image size and user parameters.
-     * 
      * Sets scaleX, scaleY, imgArea.
      * 
      * @return
@@ -182,10 +216,65 @@
     public void prepareScaleParams() throws IOException, ImageOpException {
         // logger.debug("get_scaleXY()");
 
-    	/*
+        /*
+         * calculate scaling factors
+         */
+		if (isScaleToFit()) {
+		    /* 
+		     * scale to fit -- scale factor based on destination size dw/dh and user area 
+		     * using a uniform scale factor for x and y.
+		     */
+		    imgArea = prepareScaleToFit();
+		    
+        } else if (isSqueezeToFit()) {
+            /*
+             * squeeze to fit -- scale factor based on destination size and user area
+             * 
+             * uses separate scale factors for x and y
+             */
+            imgArea = prepareSqueezeToFit();
+            
+        } else if (isCropToFit()) {
+            /*
+             * crop to fit -- don't scale
+             */
+            imgArea = prepareCropToFit();
+
+        } else if (isAbsoluteScale()) {
+            /*
+             * absolute scaling factor -- either original size, based on dpi, or absolute 
+             */
+            imgArea = prepareAbsoluteScale();
+            
+        } else {
+            throw new ImageOpException("Unknown scaling mode!");
+        }
+    
+    }
+
+    /**
+     * Scale to fit: scale factor based on destination size dw/dh and user area.
+     * 
+     * Uses a uniform scale factor for x and y.
+     * Sets ScaleX and ScaleY.
+     */
+    protected Rectangle2D prepareScaleToFit() throws IOException {
+        /*
+         * prepare minimum source image size
+         * 
+         * minSourceSize: w_min = dw * 1/ww
+         * 
+         * Note: dw or dh can be empty (=0) 
+         */
+        float scale = (1 / Math.min(getWw(), getWh()));
+        minSourceSize = new ImageSize(
+                Math.round(getAsInt("dw") * scale), 
+                Math.round(getAsInt("dh") * scale));
+        
+        /*
          * get image region of interest
          */
-        // size of the currently selected input image
+        // size of the currently selected input image (uses minSourceSize)
         imgSize  = getImgSize();
         // transform from relative [0,1] to image coordinates.
         double areaXf = getWx() * imgSize.getWidth();
@@ -199,123 +288,203 @@
         long areaWidth = Math.round(areaWidthF);
 
         /*
-         * calculate scaling factors
+         * calculate scale factors
          */
-		if (isScaleToFit()) {
-            /*
-             * scale to fit -- scaling factor based on destination size and user area
-             */
-            scaleX = getDw() / (double) areaWidth;
-            scaleY = getDh() / (double) areaHeight;
-            if (scaleX == 0) {
-            	// dw undefined
-            	scaleX = scaleY;
-            } else if (scaleY == 0) {
-            	// dh undefined
-            	scaleY = scaleX;
+        scaleX = getDw() / (double) areaWidth;
+        scaleY = getDh() / (double) areaHeight;
+        if (scaleX == 0) {
+            // dw undefined
+            scaleX = scaleY;
+        } else if (scaleY == 0) {
+            // dh undefined
+            scaleY = scaleX;
+        } else {
+            // use the smaller factor to get fit-in-box
+            if (scaleX > scaleY) {
+                scaleX = scaleY;
+                if (hasOption("fill")) {
+                    // fill mode uses whole destination rect
+                    // TODO: should we center, clip or shift the area?
+                    areaWidth = (long) (getDw() / scaleX);
+                }
             } else {
-                // use the smaller factor to get fit-in-box
-            	if (scaleX > scaleY) {
-            		scaleX = scaleY;
-            		if (hasOption("fill")) {
-            			// fill mode uses whole destination rect
-            			// TODO: should we center, clip or shift the area?
-            			areaWidth = (long) (getDw() / scaleX);
-            		}
-            	} else {
-            		scaleY = scaleX;
-            		if (hasOption("fill")) {
-            			// fill mode uses whole destination rect
-            			// TODO: should we center, clip or shift the area?
-            			areaHeight = (long) (getDh() / scaleY);
-            		}
-            	}
-            }
-            
-        } else if (isSqueezeToFit()) {
-            /*
-             * squeeze to fit -- scaling factor based on destination size and user area
-             */
-            scaleX = getDw() / (double) areaWidth;
-            scaleY = getDh() / (double) areaHeight;
-            
-        } else if (isCropToFit()){
-            /*
-             * crop to fit -- don't scale
-             */
-            areaWidth = getDw();
-            areaHeight = getDh();
-            scaleX = 1d;
-            scaleY = 1d;
-
-        } else if (isAbsoluteScale()) {
-            /*
-             * absolute scaling factor -- either original size, based on dpi, or absolute 
-             */
-            if (hasOption("osize")) {
-                /*
-                 * get original resolution from metadata
-                 */
-                imageSet.checkMeta();
-                double origResX = imageSet.getResX();
-                double origResY = imageSet.getResY();
-                if ((origResX == 0) || (origResY == 0)) {
-                    throw new ImageOpException("Missing image DPI information!");
-                }
-                double ddpix = getAsFloat("ddpix");
-                double ddpiy = getAsFloat("ddpiy");
-                if (ddpix == 0 || ddpiy == 0) {
-                    double ddpi = getAsFloat("ddpi");
-                    if (ddpi == 0) {
-                        throw new ImageOpException("Missing display DPI information!");
-                    } else {
-                        ddpix = ddpi;
-                        ddpiy = ddpi;
-                    }
-                }
-                // calculate absolute scale factor
-                scaleX = ddpix / origResX;
-                scaleY = ddpiy / origResY;
-                
-            } else {
-                /*
-                 * explicit absolute scale factor
-                 */
-                double scaleXY = (double) getAsFloat("scale");
-                scaleX = scaleXY;
-                scaleY = scaleXY;
-                // use original size if no destination size given
-                if (getDw() == 0 && getDh() == 0) {
-                    paramDW = (int) areaWidth;
-                    paramDH = (int) areaHeight;
+                scaleY = scaleX;
+                if (hasOption("fill")) {
+                    // fill mode uses whole destination rect
+                    // TODO: should we center, clip or shift the area?
+                    areaHeight = (long) (getDh() / scaleY);
                 }
             }
+        }
+        
+        return new Rectangle2D.Double(areaX, areaY, areaWidth, areaHeight);
+    }
+
+    /**
+     * Squeeze to fit: scale factor based on destination size and user area.
+     * 
+     * Uses separate scale factors for x and y
+     * Sets ScaleX and ScaleY.
+     */
+    protected Rectangle2D prepareSqueezeToFit() throws IOException {
+        /*
+         * calculate minimum source size
+         * 
+         * w_min = dw * 1/ww
+         */
+        minSourceSize = new ImageSize(
+                Math.round(getAsInt("dw") / getWw()), 
+                Math.round(getAsInt("dh") / getWh()));
+        
+        /*
+         * get image region of interest
+         */
+        // size of the currently selected input image (uses minSourceSize)
+        imgSize  = getImgSize();
+        // transform from relative [0,1] to image coordinates.
+        double areaXf = getWx() * imgSize.getWidth();
+        double areaYf = getWy() * imgSize.getHeight();
+        double areaWidthF = getWw() * imgSize.getWidth();
+        double areaHeightF = getWh() * imgSize.getHeight();
+        // round to pixels
+        long areaX = Math.round(areaXf);
+        long areaY = Math.round(areaYf);
+        long areaHeight = Math.round(areaHeightF);
+        long areaWidth = Math.round(areaWidthF);
+
+        /*
+         * calculate scale factors
+         */
+        scaleX = getDw() / (double) areaWidth;
+        scaleY = getDh() / (double) areaHeight;
+        
+        return new Rectangle2D.Double(areaX, areaY, areaWidth, areaHeight);
+    }
+
+    
+    /**
+     * Absolute scale factor: either original size, based on dpi, or absolute. 
+     * 
+     * Uses a uniform scale factor for x and y.
+     * Sets ScaleX and ScaleY.
+     * @throws ImageOpException 
+     */
+    protected Rectangle2D prepareAbsoluteScale() throws IOException, ImageOpException {
+        /*
+         * minimum source size -- apply scale to hires size
+         */
+        minSourceSize = getHiresSize().getScaled(getAsFloat("scale"));
+        
+        /*
+         * get image region of interest
+         */
+        // size of the currently selected input image (uses minSourceSize)
+        imgSize  = getImgSize();
+        // transform from relative [0,1] to image coordinates.
+        double areaXf = getWx() * imgSize.getWidth();
+        double areaYf = getWy() * imgSize.getHeight();
+        double areaWidthF = getWw() * imgSize.getWidth();
+        double areaHeightF = getWh() * imgSize.getHeight();
+        // round to pixels
+        long areaX = Math.round(areaXf);
+        long areaY = Math.round(areaYf);
+        long areaHeight = Math.round(areaHeightF);
+        long areaWidth = Math.round(areaWidthF);
+
+        /*
+         * absolute scale factor -- either original size, based on dpi, or absolute 
+         */
+        if (hasOption("osize")) {
             /*
-             * correct scaling factor if we use a pre-scaled image
+             * get original resolution from metadata
              */
-            hiresSize = getHiresSize();
-            if (imgSize.getWidth() != hiresSize.getWidth()) {
-                double preScale = (double) hiresSize.getWidth() / (double) imgSize.getWidth();
-				scaleX *= preScale;
-				scaleY *= preScale;
+            imageSet.checkMeta();
+            double origResX = imageSet.getResX();
+            double origResY = imageSet.getResY();
+            if ((origResX == 0) || (origResY == 0)) {
+                throw new ImageOpException("Missing image DPI information!");
             }
-            areaWidth = Math.round(getDw() / scaleX);
-            areaHeight = Math.round(getDh() / scaleY);
+            double ddpix = getAsFloat("ddpix");
+            double ddpiy = getAsFloat("ddpiy");
+            if (ddpix == 0 || ddpiy == 0) {
+                double ddpi = getAsFloat("ddpi");
+                if (ddpi == 0) {
+                    throw new ImageOpException("Missing display DPI information!");
+                } else {
+                    ddpix = ddpi;
+                    ddpiy = ddpi;
+                }
+            }
+            // calculate absolute scale factor
+            scaleX = ddpix / origResX;
+            scaleY = ddpiy / origResY;
             
         } else {
-            throw new ImageOpException("Unknown scaling mode!");
+            /*
+             * explicit absolute scale factor
+             */
+            double scaleXY = (double) getAsFloat("scale");
+            scaleX = scaleXY;
+            scaleY = scaleXY;
+            // use original size if no destination size given
+            if (getDw() == 0 && getDh() == 0) {
+                paramDW = (int) areaWidth;
+                paramDH = (int) areaHeight;
+            }
         }
+        /*
+         * correct absolute scale factor if we use a pre-scaled image
+         */
+        hiresSize = getHiresSize();
+        if (imgSize.getWidth() != hiresSize.getWidth()) {
+            double preScale = (double) hiresSize.getWidth() / (double) imgSize.getWidth();
+            scaleX *= preScale;
+            scaleY *= preScale;
+        }
+        areaWidth = Math.round(getDw() / scaleX);
+        areaHeight = Math.round(getDh() / scaleY);
+        
+        return new Rectangle2D.Double(areaX, areaY, areaWidth, areaHeight);
+    }
 
-		/*
-		 * set image area
-		 */
-		imgArea = new Rectangle2D.Double(areaX, areaY, areaWidth, areaHeight);
-    
-    }
-    
 
     /**
-     * Returns the mime-type of the input.
+     * Crop to fit: don't scale.
+     * 
+     * Sets ScaleX and ScaleY.
+     */
+    protected Rectangle2D prepareCropToFit() throws IOException {
+        /*
+         * minimum source size = hires size
+         */
+        minSourceSize = getHiresSize();
+        
+        /*
+         * get image region of interest
+         */
+        // size of the currently selected input image (uses minSourceSize)
+        imgSize  = getImgSize();
+        // transform from relative [0,1] to image coordinates.
+        double areaXf = getWx() * imgSize.getWidth();
+        double areaYf = getWy() * imgSize.getHeight();
+        // round to pixels
+        long areaX = Math.round(areaXf);
+        long areaY = Math.round(areaYf);
+
+        /*
+         * crop to fit -- don't scale
+         */
+        int areaWidth = getDw();
+        int areaHeight = getDh();
+        scaleX = 1d;
+        scaleY = 1d;
+        
+        return new Rectangle2D.Double(areaX, areaY, areaWidth, areaHeight);
+    }
+
+    
+    /**
+     * Return the mime-type of the input.
      * 
      * @return
      * @throws IOException
@@ -373,6 +542,8 @@
     /**
      * Returns the ImageInput to use.
      * 
+     * Note: uses getMinSourceSize().
+     * 
      * @return
      * @throws IOException
      */
@@ -408,7 +579,7 @@
     }
 
     /**
-     * Returns the DocuDirectory for the input (file).
+     * Return the DocuDirectory for the input (file).
      * 
      * @return
      * @throws FileOpException
@@ -425,7 +596,7 @@
     }
 
     /**
-     * Returns the ImageSet to load.
+     * Return the ImageSet to load.
      * 
      * @return
      * @throws FileOpException
@@ -454,7 +625,7 @@
     
     
     /**
-     * Returns the file path name from the request.
+     * Return the file path name from the request.
      * 
      * @return
      */
@@ -523,60 +694,28 @@
     }
 
     /**
-     * Returns the minimum size the source image should have for scaling.
+     * Return the minimum size the source image should have for scaling.
      * 
-     * This function is called by getInput(). It must not assume a selected input image!
+     * Note: this function is called by getInput(). It must not assume a selected input image!
      * 
      * @return
      * @throws IOException
      */
     public ImageSize getMinSourceSize() throws IOException {
         //logger.debug("getMinSourceSize()");
-        if (minSourceSize != null) {
-        	return minSourceSize;
-        }
-        
-        minSourceSize = new ImageSize();
-        if (isScaleToFit()) {
-        	/*
-        	 * scale to fit -- calculate minimum source size
-        	 * 
-        	 * roughly: w_min = dw * 1/ww
-        	 * 
-        	 * Note: dw or dh can be empty (=0) 
-        	 */
-        	float scale = (1 / Math.min(getWw(), getWh()));
-        	minSourceSize.setSize(
-        			Math.round(getAsInt("dw") * scale), 
-        			Math.round(getAsInt("dh") * scale));
-        	
-        } else if (isSqueezeToFit()) {
-        	/*
-        	 * squeeze to fit -- calculate minimum source size
-        	 * 
-        	 * w_min = dw * 1/ww
-        	 */
-        	minSourceSize.setSize(
-        			Math.round(getAsInt("dw") / getWw()), 
-        			Math.round(getAsInt("dh") / getWh()));
-        	
-        } else if (isAbsoluteScale() && hasOption("ascale")) {
-        	/*
-        	 * absolute scale -- apply scale to hires size
-        	 */
-        	minSourceSize = getHiresSize().getScaled(getAsFloat("scale"));
-        	
-        } else {
-        	/*
-        	 * clip or other -- source = hires size
-        	 */
-        	minSourceSize = getHiresSize();
+        if (minSourceSize == null) {
+            // this should not happen, it may lead to a loop!
+            logger.warn("MinSourceSize is not set! Calling prepareScaleParams again.");
+            try {
+                prepareScaleParams();
+            } catch (ImageOpException e) {
+            }
         }
         return minSourceSize;
     }
 
     /**
-     * Returns the size of the highest resolution image.
+     * Return the size of the highest resolution image.
      * 
      * @return
      * @throws IOException
@@ -595,7 +734,7 @@
     }
 
     /**
-     * Returns the size of the selected input image.
+     * Return the size of the selected input image.
      * 
      * @return
      * @throws IOException
@@ -638,40 +777,37 @@
     }
 
     /**
-     * Returns the width of the destination image.
-     * Uses dh parameter and aspect ratio if dw parameter is empty.
+     * Return the width of the destination image.
+     * Uses dw parameter.
      * 
      * @return
-     * @throws IOException
      */
-    public int getDw() throws IOException {
+    public int getDw() {
         //logger.debug("get_paramDW()");
         if (paramDW == null) {
             paramDW = getAsInt("dw");
-            paramDH = getAsInt("dh");
         }
         return paramDW;
     }
 
     /**
-     * Returns the height of the destination image.
-     * Uses dw parameter and aspect ratio if dh parameter is empty.
+     * Return the height of the destination image.
+     * Uses dh parameter.
      * 
      * @return
-     * @throws IOException
      */
-    public int getDh() throws IOException {
+    public int getDh() {
         //logger.debug("get_paramDH()");
         if (paramDH == null) {
-            paramDW = getAsInt("dw");
             paramDH = getAsInt("dh");
         }
         return paramDH;
     }
 
     /**
-     * Returns the relative width of the image area.
+     * Return the relative width of the image area.
      * Uses ww parameter.
+     * Converts ww in pixels to relative. 
      * 
      * @return
      * @throws IOException
@@ -690,8 +826,9 @@
     }
 
     /**
-     * Returns the relative height of the image area.
+     * Return the relative height of the image area.
      * Uses wh parameter.
+     * Converts wh in pixels to relative. 
      * 
      * @return
      * @throws IOException
@@ -710,8 +847,9 @@
     }
 
     /**
-     * Returns the relative x-offset of the image area.
+     * Return the relative x-offset of the image area.
      * Uses wx parameter.
+     * Converts wx in pixels to relative. 
      * 
      * @return
      * @throws IOException
@@ -730,8 +868,9 @@
     }
 
     /**
-     * Returns the relative y-offset of the image area.
+     * Return the relative y-offset of the image area.
      * Uses wy parameter.
+     * Converts wy in pixels to relative. 
      * 
      * @return
      * @throws IOException
@@ -750,7 +889,7 @@
     }
 
     /**
-     * Returns image quality as an integer.
+     * Return image quality as an integer.
      * 
      * @return
      */
@@ -785,7 +924,7 @@
     }
 
     /**
-     * Returns the maximal area of the source image that will be used.
+     * Return the maximum area of the source image that will be used.
      * 
      * This was meant to include extra pixels outside the 
      * imgArea when rotating by oblique angles but is not yet implemented.
@@ -828,10 +967,11 @@
      * @return
      */
     public float[] getRGBM() {
-        float[] paramRGBM = null;// {0f,0f,0f};
-        Parameter p = params.get("rgbm");
-        if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
-            return p.parseAsFloatArray("/");
+        if (paramRGBM == null) {
+            Parameter p = params.get("rgbm");
+            if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
+                paramRGBM = p.parseAsFloatArray("/");
+            }
         }
         return paramRGBM;
     }
@@ -842,10 +982,11 @@
      * @return
      */
     public float[] getRGBA() {
-        float[] paramRGBA = null;// {0f,0f,0f};
-        Parameter p = params.get("rgba");
-        if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
-            paramRGBA = p.parseAsFloatArray("/");
+        if (paramRGBA == null) {
+            Parameter p = params.get("rgba");
+            if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
+                paramRGBA = p.parseAsFloatArray("/");
+            }
         }
         return paramRGBA;
     }
--- a/common/src/main/java/digilib/image/ImageWorker.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/common/src/main/java/digilib/image/ImageWorker.java	Fri Nov 13 13:09:08 2015 +0100
@@ -78,7 +78,6 @@
         docuImage.setQuality(jobinfo.getScaleQual());
 
         // get area of interest and scale factor
-        jobinfo.prepareScaleParams();
         Rectangle loadRect = jobinfo.getOuterImgArea().getBounds();
         double scaleX = jobinfo.getScaleX();
         double scaleY = jobinfo.getScaleY();
--- a/common/src/main/java/digilib/io/ImageSet.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/common/src/main/java/digilib/io/ImageSet.java	Fri Nov 13 13:09:08 2015 +0100
@@ -35,6 +35,9 @@
 /**
  * Set of ImageInputs of the same image in different resolutions.
  * 
+ * The images are be added in the order of higher to lower resolutions.
+ * The first image is considered the hires "original".
+ * 
  * @author casties
  */
 public class ImageSet {
@@ -101,11 +104,10 @@
 	 * @return
 	 */
 	public ImageInput getNextSmaller(ImageSize size) {
-		for (ListIterator<ImageInput> i = getHiresIterator(); i.hasNext();) {
-			ImageInput f = i.next();
-            ImageSize is = f.getSize();
+        for (ImageInput i : list) {
+            ImageSize is = i.getSize();
             if (is != null && is.isTotallySmallerThan(size)) {
-				return f;
+				return i;
 			}
 		}
 		return null;
--- a/doc/src/site/markdown/iiif-api.md	Tue Nov 10 14:41:27 2015 +0100
+++ b/doc/src/site/markdown/iiif-api.md	Fri Nov 13 13:09:08 2015 +0100
@@ -2,23 +2,23 @@
 
 The Scaler servlet provides not only its native [Scaler API](scaler-api.html) but also an API compliant to the standards of the International Image Interoperability Framework http://iiif.io.
 
-As of version 2.3 digilib supports the [IIIF Image API version 1.1](http://iiif.io/api/image/1.1/) at [compliance level 2](http://iiif.io/api/image/1.1/compliance.html) (since V2.3.3 even for forced w,h sizes where the image will be distorted).
+As of version 2.3.6 digilib supports the [IIIF Image API version 2](http://iiif.io/api/image/2.0/) at [compliance level 2](http://iiif.io/api/image/2.0/compliance.html) (except bitonal quality). You can switch between API version 1.1 and 2.0 support with the `iiif-api-version` parameter in [digilib-config](digilib-config.html),  
 
 IIIF Image API URLs for an image request have the form:
 
-    http[s]://server/digilib-webapp/Scaler/iiif-prefix/identifier/region/size/rotation/quality[.format] 
+    http[s]://{server}/{digilib-webapp}/Scaler/{iiif-prefix}/{identifier}/{region}/{size}/{rotation}/{quality}.{format} 
 
 where `digilib-webapp` is the name of the digilib web application in the servlet container. 
 
-The value of `iiif-prefix` is defined by the `iiif-prefix` parameter in the [digilib-config](digilib-config.html). The default value is "IIIF".
+The value of `iiif-prefix` is defined by the `iiif-prefix` parameter in [digilib-config](digilib-config.html). The default value is "IIIF".
 
 The `identifier` part of the URL must not contain slashes. Since the identifier is mapped to the digilib fn-parameter, which is a filesystem path that likely contains slashes separating subdirectories, all occurrences of a slash have to be replaced by the value of the `iiif-slash-replacement` parameter in [digilib-config](digilib-config.html). The default value of the replacement string is "!", so the fn-path "books/book1/page0002" becomes the identifier "books!book1!page0002".
 
-For a definition of the other request parameters `region`, `size`, `rotation`, `quality`, and `format` please see the [IIIF Image API docs](http://iiif.io/api/image/1.1/).
+For a definition of the other parameters `region`, `size`, `rotation`, `quality`, and `format` please see the [IIIF Image API docs](http://iiif.io/api/image/2.0/).
 
 A IIIF Image API image request URL could look like:
 
-    http://www.example.org/digilib/Scaler/IIIF/books!book1!page0002/full/!150,75/0/native.jpg
+    http://www.example.org/digilib/Scaler/IIIF/books!book1!page0002/full/!150,75/0/default.jpg
 
 An info request URL for the same image looks like: 
 
--- a/pdf/src/main/java/digilib/conf/PDFRequest.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/pdf/src/main/java/digilib/conf/PDFRequest.java	Fri Nov 13 13:09:08 2015 +0100
@@ -1,5 +1,7 @@
 package digilib.conf;
 
+import java.io.IOException;
+
 /*
  * #%L
  * A container class for storing a set of instruction parameters 
@@ -33,6 +35,7 @@
 import org.apache.log4j.Logger;
 
 import digilib.image.ImageJobDescription;
+import digilib.image.ImageOpException;
 import digilib.io.DocuDirectory;
 import digilib.io.FileOpException;
 import digilib.util.NumRange;
@@ -72,9 +75,10 @@
 	 * 
 	 * @param dlcfg		The DigilibConfiguration. 		
 	 * @param request
-	 * @throws FileOpException 
+	 * @throws ImageOpException 
+	 * @throws IOException 
 	 */
-	public PDFRequest(HttpServletRequest request, DigilibConfiguration dlcfg) throws FileOpException {
+	public PDFRequest(HttpServletRequest request, DigilibConfiguration dlcfg) throws IOException, ImageOpException {
 		super(30);
 		dlConfig = dlcfg;
 		initParams();
@@ -105,9 +109,10 @@
 	 * Read the request object.
 	 * 
 	 * @param request
-	 * @throws FileOpException 
+     * @throws ImageOpException 
+     * @throws IOException 
 	 */
-	public void setWithRequest(HttpServletRequest request) throws FileOpException {
+	public void setWithRequest(HttpServletRequest request) throws IOException, ImageOpException {
 	    // read matching request parameters for the parameters in this map 
 		for (String k : params.keySet()) {
 			if (request.getParameterMap().containsKey(k)) {
@@ -145,7 +150,7 @@
 	}
 
 	
-	public ImageJobDescription getImageJobInformation(){
+	public ImageJobDescription getImageJobInformation() throws IOException, ImageOpException{
 		return ImageJobDescription.getInstance(this, dlConfig);
 	}
 	
--- a/pdf/src/main/java/digilib/pdf/PDFStreamWorker.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/pdf/src/main/java/digilib/pdf/PDFStreamWorker.java	Fri Nov 13 13:09:08 2015 +0100
@@ -40,6 +40,7 @@
 
 import digilib.image.DocuImage;
 import digilib.image.ImageJobDescription;
+import digilib.image.ImageOpException;
 import digilib.image.ImageWorker;
 import digilib.conf.DigilibConfiguration;
 import digilib.conf.PDFRequest;
@@ -85,9 +86,10 @@
 	 * @throws InterruptedException
 	 * @throws ExecutionException
 	 * @throws IOException
+	 * @throws ImageOpException 
 	 */
 	protected OutputStream renderPDF() throws DocumentException, InterruptedException,
-			ExecutionException, IOException {
+			ExecutionException, IOException, ImageOpException {
 		// create document object
 		doc = new Document(PageSize.A4, 0, 0, 0, 0);
 		PdfWriter docwriter = null;
@@ -147,7 +149,13 @@
 	 */
 	public Document addTitlePage(Document doc) throws DocumentException {
 		PDFTitlePage titlepage = new PDFTitlePage(job_info);
-		doc.add(titlepage.getPageContents());
+		try {
+            doc.add(titlepage.getPageContents());
+        } catch (IOException e) {
+            throw new DocumentException(e);
+        } catch (ImageOpException e) {
+            throw new DocumentException(e);
+        }
 		doc.newPage();
 		return doc;
 	}
--- a/pdf/src/main/java/digilib/pdf/PDFTitlePage.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/pdf/src/main/java/digilib/pdf/PDFTitlePage.java	Fri Nov 13 13:09:08 2015 +0100
@@ -40,6 +40,7 @@
 import com.itextpdf.text.Paragraph;
 
 import digilib.conf.PDFRequest;
+import digilib.image.ImageOpException;
 import digilib.io.FileOpException;
 import digilib.servlet.PDFCache;
 
@@ -78,6 +79,10 @@
             return new DigilibInfoReader(infoFn.getAbsolutePath());
         } catch (FileOpException e) {
             logger.warn("info.xml not found");
+        } catch (IOException e) {
+            logger.warn("image directory for info.xml not found");
+        } catch (ImageOpException e) {
+            logger.warn("problem with parameters for info.xml");
         }
         return null;
     }
@@ -86,8 +91,10 @@
 	 * generate iText-PDF-Contents for the title page
 	 * 
 	 * @return
+	 * @throws ImageOpException 
+	 * @throws IOException 
 	 */
-	public Element getPageContents(){
+	public Element getPageContents() throws IOException, ImageOpException{
 		Paragraph content = new Paragraph();
 		content.setAlignment(Element.ALIGN_CENTER);
 
@@ -165,7 +172,7 @@
 		return null;
 	}
 	
-	private String getTitle(){
+	private String getTitle() throws IOException, ImageOpException {
 		if(info_reader.hasInfo())
 			return info_reader.getAsString("title");
 		else
--- a/servlet/src/main/java/digilib/servlet/ServletOps.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/servlet/src/main/java/digilib/servlet/ServletOps.java	Fri Nov 13 13:09:08 2015 +0100
@@ -47,6 +47,7 @@
 import digilib.io.FileOpException;
 import digilib.io.FileOps;
 import digilib.io.ImageInput;
+import digilib.io.ImageSet;
 import digilib.util.ImageSize;
 
 public class ServletOps {
@@ -379,11 +380,16 @@
             logger.error("No response!");
             return;
         }
+        
+        /*
+         * get image size
+         */
         ImageSize size = null;
+        ImageSet imageSet = null;
         try {
             // get original image size
-            ImageInput img;
-            img = dlReq.getJobDescription().getImageSet().getBiggest();
+            imageSet = dlReq.getJobDescription().getImageSet();
+            ImageInput img = imageSet.getBiggest();
             size = img.getSize();
         } catch (FileOpException e) {
             try {
@@ -393,26 +399,83 @@
                 throw new ServletException("Unable to write error response!", e);
             }
         }
+        
+        /*
+         * get resource URL
+         */
         String url = dlReq.getServletRequest().getRequestURL().toString();
         if (url.endsWith("/info.json")) {
             url = url.substring(0, url.lastIndexOf("/info.json"));
         } else if (url.endsWith("/")) {
             url = url.substring(0, url.lastIndexOf("/"));
         }
+        
+        /*
+         * send response
+         */
         response.setCharacterEncoding("UTF-8");
-        response.setContentType("application/json,application/ld+json");
-        PrintWriter writer;
+        logger.debug("sending info.json");
         try {
-            writer = response.getWriter();
-            writer.println("{");
-            writer.println("\"@context\" : \"http://library.stanford.edu/iiif/image-api/1.1/context.json\",");
-            writer.println("\"@id\" : \"" + url + "\",");
-            writer.println("\"width\" : " + size.width + ",");
-            writer.println("\"height\" : " + size.height + ",");
-            writer.println("\"formats\" : [\"jpg\", \"png\"],");
-            writer.println("\"qualities\" : [\"native\", \"color\", \"grey\"],");
-            writer.println("\"profile\" : \"http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level2\"");
-            writer.println("}");
+            PrintWriter writer;
+            if (dlReq.getDigilibConfig().getAsString("iiif-api-version").startsWith("2.")) {
+                /*
+                 * IIIF Image API version 2 image information
+                 */
+                // use JSON-LD content type only when asked
+                String accept = dlReq.getServletRequest().getHeader("Accept");
+                if (accept != null && accept.contains("application/ld+json")) {
+                    response.setContentType("application/ld+json");
+                } else {
+                    response.setContentType("application/json");
+                    response.setHeader("Link", "<http://iiif.io/api/image/2/context.json>"
+                            +"; rel=\"http://www.w3.org/ns/json-ld#context\""
+                            +"; type=\"application/ld+json\"");
+                }
+                // write info.json
+                writer = response.getWriter();
+                writer.println("{");
+                writer.println("\"@context\" : \"http://iiif.io/api/image/2/context.json\",");
+                writer.println("\"@id\" : \"" + url + "\",");
+                writer.println("\"@protocol\" : \"http://iiif.io/api/image\",");
+                writer.println("\"width\" : " + size.width + ",");
+                writer.println("\"height\" : " + size.height + ",");
+                writer.println("\"profile\" : [");
+                writer.println("  \"http://iiif.io/api/image/2/level2.json\",");
+                writer.println("  {");
+                writer.println("    \"formats\" : [\"jpg\", \"png\"],");
+                writer.println("    \"qualities\" : [\"color\", \"gray\"],");
+                writer.println("    \"supports\" : [\"mirroring\", \"rotationArbitrary\", \"sizeAboveFull\"]");
+                writer.println("  }]");
+                // add sizes of prescaled images
+                int numImgs = imageSet.size();
+                if (numImgs > 1) {
+                    writer.println(", \"sizes\" : [");
+                    for (int i = numImgs - 1; i > 0; --i) {
+                        ImageInput ii = imageSet.get(i);
+                        ImageSize is = ii.getSize();
+                        writer.println("  {\"width\" : "+is.getWidth()+", \"height\" : "+is.getHeight()+"}"
+                                +((i > 1)?",":""));
+                    }
+                    writer.println("]");
+                }
+                writer.println("}");
+                
+            } else {
+                /*
+                 * IIIF Image API version 1 image information
+                 */
+                response.setContentType("application/json,application/ld+json");
+                writer = response.getWriter();
+                writer.println("{");
+                writer.println("\"@context\" : \"http://library.stanford.edu/iiif/image-api/1.1/context.json\",");
+                writer.println("\"@id\" : \"" + url + "\",");
+                writer.println("\"width\" : " + size.width + ",");
+                writer.println("\"height\" : " + size.height + ",");
+                writer.println("\"formats\" : [\"jpg\", \"png\"],");
+                writer.println("\"qualities\" : [\"native\", \"color\", \"grey\"],");
+                writer.println("\"profile\" : \"http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level2\"");
+                writer.println("}");
+            }
         } catch (IOException e) {
             throw new ServletException("Unable to write response!", e);
         }
--- a/servlet2/src/main/java/digilib/servlet/Scaler.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/servlet2/src/main/java/digilib/servlet/Scaler.java	Fri Nov 13 13:09:08 2015 +0100
@@ -224,9 +224,7 @@
 
         // parse request
         DigilibServletRequest dlRequest = new DigilibServletRequest(request, dlConfig);
-        // extract the job information
-        ImageJobDescription jobTicket = ImageJobDescription.getInstance(dlRequest, dlConfig);
-
+        
         // type of error reporting
         ErrMsg errMsgType = ErrMsg.IMAGE;
         if (dlRequest.hasOption("errtxt")) {
@@ -236,10 +234,13 @@
         }
 
         try {
+            // extract the job information
+            ImageJobDescription jobTicket = ImageJobDescription.getInstance(dlRequest, dlConfig);
+
             /*
              * check if we can fast-track without scaling
              */
-            ImageInput fileToLoad = (ImageInput) jobTicket.getInput();
+            ImageInput fileToLoad = jobTicket.getInput();
 
             // check permissions
             if (useAuthorization) {
--- a/servlet2/src/main/java/digilib/servlet/ScalerNoThread.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/servlet2/src/main/java/digilib/servlet/ScalerNoThread.java	Fri Nov 13 13:09:08 2015 +0100
@@ -55,7 +55,7 @@
     private static final long serialVersionUID = 1450947819851623306L;
 
     /** digilib servlet version (for all components) */
-    public static final String version = "2.3.1 nothread";
+    public static final String version = DigilibServletConfiguration.getClassVersion() + " nothread";
 
     /** servlet error codes */
     public static enum Error {
@@ -207,8 +207,6 @@
 
         // parse request
         DigilibServletRequest dlRequest = new DigilibServletRequest(request, dlConfig);
-        // extract the job information
-        ImageJobDescription jobTicket = ImageJobDescription.getInstance(dlRequest, dlConfig);
 
         // type of error reporting
         ErrMsg errMsgType = ErrMsg.IMAGE;
@@ -219,6 +217,8 @@
         }
 
         try {
+            // extract the job information
+            ImageJobDescription jobTicket = ImageJobDescription.getInstance(dlRequest, dlConfig);
             /*
              * check if we can fast-track without scaling
              */
--- a/servlet3/src/main/java/digilib/servlet/Scaler.java	Tue Nov 10 14:41:27 2015 +0100
+++ b/servlet3/src/main/java/digilib/servlet/Scaler.java	Fri Nov 13 13:09:08 2015 +0100
@@ -237,14 +237,6 @@
 
         // parse request
         DigilibServletRequest dlRequest = new DigilibServletRequest(request, dlConfig);
-        // extract the job information
-        final ImageJobDescription jobTicket = ImageJobDescription.getInstance(dlRequest, dlConfig);
-        
-        // handle the info-request
-        if (dlRequest.hasOption("info")) {
-            ServletOps.sendIiifInfo(dlRequest, response, logger);
-            return;
-        }
 
         // type of error reporting
         ErrMsg errMsgType = defaultErrMsgType;
@@ -256,17 +248,26 @@
             errMsgType = ErrMsg.CODE;
         }
 
-        // error out if request was bad
-        if (dlRequest.errorMessage != null) {
-            digilibError(errMsgType, Error.UNKNOWN, dlRequest.errorMessage, response);
-            return;
-        }
-        
         try {
-            /*
-             * get the input file
-             */
-            ImageInput fileToLoad = (ImageInput) jobTicket.getInput();
+            // extract the job information
+            final ImageJobDescription jobTicket = ImageJobDescription.getInstance(dlRequest, dlConfig);
+
+            // handle the IIIF info-request
+            if (dlRequest.hasOption("info")) {
+                ServletOps.sendIiifInfo(dlRequest, response, logger);
+                return;
+            }
+            if (dlRequest.hasOption("redirect-info")) {
+                // TODO: the redirect should have code 303
+                response.sendRedirect("info.json");
+                return;
+            }
+
+            // error out if request was bad
+            if (dlRequest.errorMessage != null) {
+                digilibError(errMsgType, Error.UNKNOWN, dlRequest.errorMessage, response);
+                return;
+            }
 
             /*
              * check permissions
@@ -280,6 +281,11 @@
             }
 
             /*
+             * get the input file
+             */
+            ImageInput fileToLoad = jobTicket.getInput();
+
+            /*
              * if requested, send image as a file
              */
             if (sendFileAllowed && jobTicket.getSendAsFile()) {
--- a/webapp/pom.xml	Tue Nov 10 14:41:27 2015 +0100
+++ b/webapp/pom.xml	Fri Nov 13 13:09:08 2015 +0100
@@ -11,6 +11,10 @@
 	<description>The Digital Image Library - web application server and HTML and JS clients.</description>
 	<url>http://digilib.sourceforge.net</url>
 	<packaging>war</packaging>
+	
+	<properties>
+        <skipTests>true</skipTests>
+    </properties>
 
 	<build>
 		<pluginManagement>
@@ -31,7 +35,15 @@
 						</includes>
 					</configuration>
 				</plugin>
-			</plugins>
+		        <plugin>
+			        <groupId>org.apache.maven.plugins</groupId>
+			        <artifactId>maven-surefire-plugin</artifactId>
+			        <version>2.19</version>
+			        <configuration>
+			            <skip>${skipTests}</skip>
+			        </configuration>
+			    </plugin>
+        	</plugins>
 		</pluginManagement>
 	</build>
 	<profiles>
@@ -157,5 +169,39 @@
 				</dependency>
 			</dependencies>
 		</profile>
+        <profile>
+            <id>cors-filter</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+            <!--  external servlet filter to add CORS headers -->
+            <dependencies>
+                <dependency>
+                    <groupId>org.eclipse.jetty</groupId>
+                    <artifactId>jetty-servlets</artifactId>
+                    <version>9.2.13.v20150730</version>
+                </dependency>
+            </dependencies>
+        </profile>
 	</profiles>
+	<dependencies>
+		<dependency>
+			<groupId>org.eclipse.jetty</groupId>
+			<artifactId>jetty-servlet</artifactId>
+            <version>9.2.13.v20150730</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.eclipse.jetty</groupId>
+			<artifactId>jetty-http</artifactId>
+			<version>9.2.13.v20150730</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<version>4.12</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
 </project>
--- a/webapp/src/main/webapp/WEB-INF/web-3.0.xml	Tue Nov 10 14:41:27 2015 +0100
+++ b/webapp/src/main/webapp/WEB-INF/web-3.0.xml	Fri Nov 13 13:09:08 2015 +0100
@@ -50,4 +50,18 @@
             /Scaler/*
         </url-pattern>
   </servlet-mapping>
+
+	<!-- add CORS headers -->
+	<filter>
+		<filter-name>CORS</filter-name>
+		<!-- use either Tomcat's or Jetty's filter class -->
+		<!-- <filter-class>org.apache.catalina.filters.CorsFilter</filter-class> -->
+		<filter-class>org.eclipse.jetty.servlets.CrossOriginFilter</filter-class>
+		<async-supported>true</async-supported>
+	</filter>
+	<filter-mapping>
+		<filter-name>CORS</filter-name>
+		<url-pattern>/*</url-pattern>
+	</filter-mapping>
+
 </web-app>
--- a/webapp/src/main/webapp/WEB-INF/web-additional.xml	Tue Nov 10 14:41:27 2015 +0100
+++ b/webapp/src/main/webapp/WEB-INF/web-additional.xml	Fri Nov 13 13:09:08 2015 +0100
@@ -75,4 +75,17 @@
         </form-login-config>
     </login-config>
 
+    <!-- add CORS headers -->
+    <filter>
+        <filter-name>CORS</filter-name>
+        <!-- use either Tomcat's or Jetty's filter class -->
+        <!-- <filter-class>org.apache.catalina.filters.CorsFilter</filter-class> -->
+        <filter-class>org.eclipse.jetty.servlets.CrossOriginFilter</filter-class>
+        <async-supported>true</async-supported>
+    </filter>
+    <filter-mapping>
+        <filter-name>CORS</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
 </web-app>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webapp/src/test/java/digilib/servlet/ScalerTest.java	Fri Nov 13 13:09:08 2015 +0100
@@ -0,0 +1,177 @@
+package digilib.servlet;
+
+/*
+ * #%L
+ * ScalerTest -- tests for the digilib Scaler servlet
+ * 
+ * Digital Image Library servlet components
+ * 
+ * %%
+ * Copyright (C) 2015 MPIWG Berlin
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as 
+ * published by the Free Software Foundation, either version 3 of the 
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Lesser Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Lesser Public 
+ * License along with this program.  If not, see
+ * <http://www.gnu.org/licenses/lgpl-3.0.html>.
+ * #L%
+ * Author: Robert Casties (robcast@users.sourceforge.net)
+ */
+
+import static org.junit.Assert.assertEquals;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import javax.imageio.ImageIO;
+
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletTester;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import digilib.conf.DigilibServlet3Configuration;
+
+/**
+ * ScalerTest -- tests for the digilib Scaler servlet
+ * 
+ * @author casties
+ *
+ */
+public class ScalerTest {
+
+    private static ServletTester tester;
+    
+    public static String testFileName = "xterm_color_chart";
+
+    @BeforeClass
+    public static void startServer() throws Exception {
+        tester = new ServletTester();
+        ServletContextHandler ctx = tester.getContext();
+        // set up ServletContext
+        ctx.setContextPath("/");
+        ctx.setResourceBase("src/main/webapp");       
+        ctx.setClassLoader(ServletTester.class.getClassLoader());
+        // add digilib ContextListener
+        DigilibServlet3Configuration dlConfig = new DigilibServlet3Configuration();
+        ctx.addEventListener(dlConfig);
+        tester.addServlet(Scaler.class, "/Scaler/*");
+        // start the servlet
+        tester.start();
+    }
+
+    /**
+     * Requests the image file testFileName from the Scaler with the given parameters and returns the image.
+     * 
+     * Checks the returned content-type (if contentType != null).
+     *  
+     * @param params
+     * @param contentType
+     * @return
+     * @throws Exception
+     * @throws IOException
+     */
+    private BufferedImage loadImage(String params, String contentType) throws Exception, IOException {
+        // prepare request
+        HttpTester.Request request = HttpTester.newRequest();
+        request.setMethod("GET");
+        request.setHeader("Host", "tester"); // should be "tester"
+        request.setURI("/Scaler?fn="+testFileName+"&"+params);
+        request.setContent("");
+        ByteBuffer reqBuf = request.generate();
+        // get response
+        ByteBuffer respBuf = tester.getResponses(reqBuf);
+        // parse response
+        HttpTester.Response response = HttpTester.parseResponse(respBuf);
+        // should be 200 - OK
+        assertEquals("status code", 200, response.getStatus());
+        // check content-type
+        if (contentType != null) {
+            String ct = response.getStringField("content-type");
+            assertEquals("content-type", contentType, ct);
+        }
+        // load response as image
+        ByteArrayInputStream bis = new ByteArrayInputStream(response.getContentBytes());
+        BufferedImage img = ImageIO.read(bis);
+        return img;
+    }
+
+    /**
+     * Test scaling with mo=fit
+     * @throws Exception
+     */
+    @Test
+    public void testScaleFit() throws Exception {
+        BufferedImage img = loadImage("ww=0.0836&wh=0.0378&wx=0&wy=0.961&dw=173&dh=235&mo=fit,errcode", null);
+        assertEquals("height", 125, img.getHeight());
+        assertEquals("width", 173, img.getWidth());
+    }
+
+    /**
+     * Test scaling with mo=squeeze
+     * @throws Exception
+     */
+    @Test
+    public void testScaleSqueeze() throws Exception {
+        BufferedImage img = loadImage("ww=0.0836&wh=0.0378&wx=0&wy=0.961&dw=173&dh=235&mo=squeeze,errcode", null);
+        assertEquals("height", 235, img.getHeight());
+        assertEquals("width", 173, img.getWidth());        
+    }
+
+    /**
+     * Test scaling with mo=clip
+     * @throws Exception
+     */
+    @Test
+    public void testScaleClip() throws Exception {
+        BufferedImage img = loadImage("ww=0.0836&wh=0.0378&wx=0&wy=0.961&dw=173&dh=235&mo=clip,errcode", null);
+        assertEquals("height", 60, img.getHeight());
+        assertEquals("width", 173, img.getWidth());        
+    }
+
+    /**
+     * Test scaling with mo=ascale
+     * @throws Exception
+     */
+    @Test
+    public void testScaleAbsolute() throws Exception {
+        BufferedImage img = loadImage("mo=ascale&scale=0.1&mo=errcode", null);
+        assertEquals("height", 154, img.getHeight());
+        assertEquals("width", 96, img.getWidth());        
+    }
+
+    /**
+     * Test forced image type with mo=jpg
+     * @throws Exception
+     */
+    @Test
+    public void testTypeJpg() throws Exception {
+        BufferedImage img = loadImage("ww=0.0836&wh=0.0378&wx=0&wy=0.961&dw=173&dh=235&mo=jpg,errcode", "image/jpeg");
+        int px = img.getRGB(100, 100);
+        assertEquals("pixel color", -8421505, px);
+    }
+
+    /**
+     * Test forced image type with mo=png
+     * @throws Exception
+     */
+    @Test
+    public void testTypePng() throws Exception {
+        BufferedImage img = loadImage("ww=0.0836&wh=0.0378&wx=0&wy=0.961&dw=173&dh=235&mo=png,errcode", "image/png");
+        int px = img.getRGB(100, 100);
+        assertEquals("pixel color", -8421505, px);
+    }
+
+
+}