changeset 1449:7f6b5e7e2afd

Merge from new_scaling branch a2da0b5caedd6005d2be15c0507e4cb17d464d8e
author robcast
date Wed, 11 Nov 2015 20:33:10 +0100
parents c4d32640c1be (current diff) a2da0b5caedd (diff)
children fa63f437d5c5
files
diffstat 15 files changed, 610 insertions(+), 186 deletions(-) [+]
line wrap: on
line diff
--- a/README.md	Tue Nov 10 15:40:18 2015 +0100
+++ b/README.md	Wed Nov 11 20:33:10 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 15:40:18 2015 +0100
+++ b/common/src/main/java/digilib/conf/DigilibConfiguration.java	Wed Nov 11 20:33:10 2015 +0100
@@ -57,7 +57,7 @@
 
     /** digilib version */
     public static String getClassVersion() {
-        return "2.3.4a";
+        return "2.3.5a";
     }
 
     /* non-static getVersion for Java inheritance */
--- a/common/src/main/java/digilib/image/ImageJobDescription.java	Tue Nov 10 15:40:18 2015 +0100
+++ b/common/src/main/java/digilib/image/ImageJobDescription.java	Wed Nov 11 20:33:10 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;
 
@@ -67,6 +94,7 @@
      */
     public ImageJobDescription(DigilibConfiguration dlcfg) {
         super(30);
+        initParams();
         dlConfig = dlcfg;
         dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
     }
@@ -138,13 +166,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 +187,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 +203,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 +214,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,121 +286,201 @@
         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);
+    /**
+     * 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);
+    }
+
     
-    }
-    
-
     /**
      * Returns the mime-type of the input.
      * 
@@ -373,6 +540,8 @@
     /**
      * Returns the ImageInput to use.
      * 
+     * uses getMinSourceSize().
+     * 
      * @return
      * @throws IOException
      */
@@ -532,45 +701,13 @@
      */
     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;
     }
--- a/common/src/main/java/digilib/image/ImageWorker.java	Tue Nov 10 15:40:18 2015 +0100
+++ b/common/src/main/java/digilib/image/ImageWorker.java	Wed Nov 11 20:33:10 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/pdf/src/main/java/digilib/conf/PDFRequest.java	Tue Nov 10 15:40:18 2015 +0100
+++ b/pdf/src/main/java/digilib/conf/PDFRequest.java	Wed Nov 11 20:33:10 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 15:40:18 2015 +0100
+++ b/pdf/src/main/java/digilib/pdf/PDFStreamWorker.java	Wed Nov 11 20:33:10 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 15:40:18 2015 +0100
+++ b/pdf/src/main/java/digilib/pdf/PDFTitlePage.java	Wed Nov 11 20:33:10 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 15:40:18 2015 +0100
+++ b/servlet/src/main/java/digilib/servlet/ServletOps.java	Wed Nov 11 20:33:10 2015 +0100
@@ -379,6 +379,10 @@
             logger.error("No response!");
             return;
         }
+        
+        /*
+         * get image size
+         */
         ImageSize size = null;
         try {
             // get original image size
@@ -393,15 +397,24 @@
                 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("{");
--- a/servlet2/src/main/java/digilib/servlet/Scaler.java	Tue Nov 10 15:40:18 2015 +0100
+++ b/servlet2/src/main/java/digilib/servlet/Scaler.java	Wed Nov 11 20:33:10 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 15:40:18 2015 +0100
+++ b/servlet2/src/main/java/digilib/servlet/ScalerNoThread.java	Wed Nov 11 20:33:10 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 15:40:18 2015 +0100
+++ b/servlet3/src/main/java/digilib/servlet/Scaler.java	Wed Nov 11 20:33:10 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,21 @@
             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 info-request
+            if (dlRequest.hasOption("info")) {
+                ServletOps.sendIiifInfo(dlRequest, response, logger);
+                return;
+            }
+
+            // error out if request was bad
+            if (dlRequest.errorMessage != null) {
+                digilibError(errMsgType, Error.UNKNOWN, dlRequest.errorMessage, response);
+                return;
+            }
 
             /*
              * check permissions
@@ -280,6 +276,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 15:40:18 2015 +0100
+++ b/webapp/pom.xml	Wed Nov 11 20:33:10 2015 +0100
@@ -13,6 +13,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>
@@ -33,7 +37,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>
@@ -159,5 +171,40 @@
 				</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>
+                    <!-- <version>9.3.5.v20151012</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 15:40:18 2015 +0100
+++ b/webapp/src/main/webapp/WEB-INF/web-3.0.xml	Wed Nov 11 20:33:10 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 15:40:18 2015 +0100
+++ b/webapp/src/main/webapp/WEB-INF/web-additional.xml	Wed Nov 11 20:33:10 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	Wed Nov 11 20:33:10 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);
+    }
+
+
+}