changeset 1424:21da78f0a267

Merge from new_scaling branch 75ec39abcbba2d5d6e3e12fc43c8a829e7dd18d7
author robcast
date Tue, 27 Oct 2015 11:23:30 +0100
parents 18690b59687e (current diff) 75ec39abcbba (diff)
children d4cfce3887f0
files webapp/src/main/webapp/WEB-INF/web-2.3.xml
diffstat 12 files changed, 381 insertions(+), 275 deletions(-) [+]
line wrap: on
line diff
--- a/common/src/main/java/digilib/conf/DigilibConfiguration.java	Mon Oct 26 14:37:47 2015 +0100
+++ b/common/src/main/java/digilib/conf/DigilibConfiguration.java	Tue Oct 27 11:23:30 2015 +0100
@@ -57,7 +57,7 @@
 
     /** digilib version */
     public static String getClassVersion() {
-        return "2.3.2";
+        return "2.3.3";
     }
 
     /* non-static getVersion for Java inheritance */
--- a/common/src/main/java/digilib/conf/DigilibRequest.java	Mon Oct 26 14:37:47 2015 +0100
+++ b/common/src/main/java/digilib/conf/DigilibRequest.java	Tue Oct 27 11:23:30 2015 +0100
@@ -520,11 +520,9 @@
                             // width only
                             setValueFromString("dw", parms[0]);
                         } else {
-                            // w,h -- according to spec, we should distort the
-                            // image to match ;-(
-                            errorMessage = "Non-uniform-scale size parameter in IIIF path not supported!";
-                            logger.error(errorMessage);
-                            return false;
+                            // w,h -- according to spec, we should distort the image to match ;-(
+                        	options.setOption("squeeze");
+                            setValueFromString("dw", parms[0]);
                         }
                     }
                     if (parms[1].length() > 0) {
--- a/common/src/main/java/digilib/image/ImageJobDescription.java	Mon Oct 26 14:37:47 2015 +0100
+++ b/common/src/main/java/digilib/image/ImageJobDescription.java	Tue Oct 27 11:23:30 2015 +0100
@@ -43,10 +43,11 @@
     protected DocuDirectory fileDir = null;
     protected DocuImage docuImage = null;
     protected String filePath = null;
-    protected ImageSize expectedSourceSize = null;
-    protected Double scaleXY = null;
-    protected Rectangle2D userImgArea = null;
-    protected Rectangle2D outerUserImgArea = null;
+    protected ImageSize minSourceSize = null;
+    protected Double scaleX = null;
+    protected Double scaleY = null;
+    protected Rectangle2D imgArea = null;
+    protected Rectangle2D outerImgArea = null;
     protected Boolean imageSendable = null;
     protected String mimeType = null;
     protected Integer paramDW = null;
@@ -57,6 +58,7 @@
     protected Float paramWH = null;
     protected DocuDirCache dirCache = null;
 	protected ImageSize hiresSize = null;
+	protected ImageSize imgSize = null;
 
     /**
      * create empty ImageJobDescription.
@@ -163,13 +165,160 @@
         return newMap;
     }
 
+    
+    /**
+     * Prepare image scaling factors and coordinates.
+     * 
+     * Uses image size and user parameters.
+     * 
+     * Sets scaleX, scaleY, imgArea.
+     * 
+     * @return
+     * @throws IOException
+     * @throws ImageOpException
+     */
+    public void prepareScaleParams() throws IOException, ImageOpException {
+        // logger.debug("get_scaleXY()");
+
+    	/*
+         * get image region of interest
+         */
+        // size of the currently selected input image
+        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 scaling 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;
+            } 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;
+                }
+            }
+            /*
+             * correct scaling 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);
+            
+        } else {
+            throw new ImageOpException("Unknown scaling mode!");
+        }
+
+		/*
+		 * set image area
+		 */
+		imgArea = new Rectangle2D.Double(areaX, areaY, areaWidth, areaHeight);
+    
+    }
+    
+
     /**
      * Returns the mime-type of the input.
      * 
      * @return
      * @throws IOException
      */
-    public String getMimeType() throws IOException {
+    public String getInputMimeType() throws IOException {
         if (mimeType == null) {
             input = getInput();
             mimeType = input.getMimetype();
@@ -192,7 +341,7 @@
         }
         // use input image type
         try {
-            String mt = getMimeType();
+            String mt = getInputMimeType();
             if ((mt.equals("image/jpeg") || mt.equals("image/jp2") || mt.equals("image/fpx"))) {
                 return "image/jpeg";
             } else {
@@ -235,14 +384,14 @@
                 input = imageSet.getBiggest();
             } else if (isLoresOnly()) {
                 // enforced lores uses next smaller resolution
-                input = imageSet.getNextSmaller(getExpectedSourceSize());
+                input = imageSet.getNextSmaller(getMinSourceSize());
                 if (input == null) {
                     // this is the smallest we have
                     input = imageSet.getSmallest();
                 }
             } else {
                 // autores: use next higher resolution
-                input = imageSet.getNextBigger(getExpectedSourceSize());
+                input = imageSet.getNextBigger(getMinSourceSize());
                 if (input == null) {
                     // this is the highest we have
                     input = imageSet.getBiggest();
@@ -316,18 +465,57 @@
         return filePath;
     }
 
+    /**
+     * Only use the highest resolution image.
+     * 
+     * @return
+     */
     public boolean isHiresOnly() {
         return hasOption("clip") || hasOption("hires");
     }
 
+    /**
+     * Prefer a prescaled lower resolution image.
+     * 
+     * @return
+     */
     public boolean isLoresOnly() {
         return hasOption("lores");
     }
 
+    /**
+     * Scale according to zoom area and destination size preserving aspect ratio.
+     * 
+     * @return
+     */
     public boolean isScaleToFit() {
-        return !(hasOption("clip") || hasOption("osize") || hasOption("ascale"));
+        return hasOption("fit") || 
+        		!(hasOption("clip") || hasOption("osize") || hasOption("ascale") || hasOption("squeeze"));
     }
 
+    /**
+     * Do not scale, just crop.
+     * 
+     * @return
+     */
+    public boolean isCropToFit() {
+        return hasOption("clip");
+    }
+
+    /**
+     * Scale according to zoom area and destination size violating aspect ratio.
+     * 
+     * @return
+     */
+    public boolean isSqueezeToFit() {
+        return hasOption("squeeze");
+    }
+
+    /**
+     * Scale according to fixed factor independent of destination size.
+     * 
+     * @return
+     */
     public boolean isAbsoluteScale() {
         return hasOption("osize") || hasOption("ascale");
     }
@@ -335,25 +523,54 @@
     /**
      * Returns the minimum size the source image should have for scaling.
      * 
+     * This function is called by getInput(). It must not assume a selected input image!
+     * 
      * @return
      * @throws IOException
      */
-    public ImageSize getExpectedSourceSize() throws IOException {
-        if (expectedSourceSize == null) {
-            expectedSourceSize = new ImageSize();
-            if (isScaleToFit()) {
-                // scale to fit -- calculate minimum source size
-                float scale = (1 / Math.min(getWw(), getWh())) * getAsFloat("ws");
-                expectedSourceSize.setSize((int) (getDw() * scale), (int) (getDh() * scale));
-            } else if (isAbsoluteScale() && hasOption("ascale")) {
-                // absolute scale -- apply scale to hires size
-                expectedSourceSize = getHiresSize().getScaled(getAsFloat("scale"));
-            } else {
-                // clip to fit -- source = destination size
-                expectedSourceSize.setSize((int) (getDw() * getAsFloat("ws")), (int) (getDh() * getAsFloat("ws")));
-            }
+    public ImageSize getMinSourceSize() throws IOException {
+        //logger.debug("getMinSourceSize()");
+        if (minSourceSize != null) {
+        	return minSourceSize;
         }
-        return expectedSourceSize;
+        
+        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();
+        }
+        return minSourceSize;
     }
 
     /**
@@ -363,7 +580,7 @@
      * @throws IOException
      */
     public ImageSize getHiresSize() throws IOException {
-        logger.debug("get_hiresSize()");
+        //logger.debug("get_hiresSize()");
         if (hiresSize == null) {
 	        ImageSet fileset = getImageSet();
 	        ImageInput hiresFile = fileset.getBiggest();
@@ -373,102 +590,46 @@
     }
 
     /**
-     * Returns image scaling factor.
-     * Uses image size and user parameters.
+     * Returns the size of the selected input image.
+     * 
+     * @return
+     * @throws IOException
+     */
+    public ImageSize getImgSize() throws IOException {
+        //logger.debug("get_hiresSize()");
+        if (imgSize == null) {
+        	imgSize = getInput().getSize();
+        }
+        return imgSize;
+    }
+
+      
+    /**
+     * Return the X scale factor.
      * 
      * @return
      * @throws IOException
      * @throws ImageOpException
      */
-    public double getScaleXY() throws IOException, ImageOpException {
-        // logger.debug("get_scaleXY()");
-        if (scaleXY != null) {
-            return scaleXY.doubleValue();
-        }
+    public double getScaleX() throws IOException, ImageOpException {
+    	if (scaleX == null) {
+    		prepareScaleParams();
+    	}
+    	return scaleX.doubleValue();
+    }
 
-        /*
-         * calculate region of interest
-         */
-        userImgArea = getUserImgArea();
-        double areaWidth = userImgArea.getWidth();
-		double areaHeight = userImgArea.getHeight();
-        
-        /*
-         * calculate scaling factor
-         */
-        float ws = getAsFloat("ws");
-		if (isScaleToFit()) {
-            /*
-             * scale to fit -- scaling factor based on destination size and user area
-             */
-            double scaleX = getDw() / areaWidth * ws;
-            double scaleY = getDh() / areaHeight * ws;
-            // use the smaller factor to get fit-in-box
-            if (scaleX == 0) {
-            	scaleXY = scaleY;
-            } else if (scaleY == 0) {
-            	scaleXY = scaleX;
-            } else {
-            	scaleXY = (scaleX > scaleY) ? scaleY : scaleX;
-            }
-        } 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
-                double sx = ddpix / origResX;
-                double sy = ddpiy / origResY;
-                // currently only same scale -- mean value
-                scaleXY = (sx + sy) / 2f;
-            } else {
-                // absolute scale factor
-                scaleXY = (double) getAsFloat("scale");
-                // use original size if no destination size given
-                if (getDw() == 0 && getDh() == 0) {
-                    paramDW = (int) areaWidth;
-                    paramDH = (int) areaHeight;
-                }
-            }
-            // we need to correct the factor if we use a pre-scaled image
-            ImageSize imgSize = getInput().getSize();
-            ImageSize hiresSize = getHiresSize();
-            if (imgSize.getWidth() != hiresSize.getWidth()) {
-                scaleXY *= (double) hiresSize.getWidth() / (double) imgSize.getWidth();
-            }
-            areaWidth = (int) Math.round(getDw() / scaleXY * ws);
-            areaHeight = (int) Math.round(getDh() / scaleXY * ws);
-            // reset user area size
-            userImgArea.setRect(userImgArea.getX(), userImgArea.getY(), areaWidth, areaHeight);
-        } else {
-            /*
-             * crop to fit -- don't scale
-             */
-            areaWidth = Math.round(getDw() * ws);
-            areaHeight = Math.round(getDh() * ws);
-            // reset user area size
-            userImgArea.setRect(userImgArea.getX(), userImgArea.getY(), areaWidth, areaHeight);
-            scaleXY = 1d;
-        }
-        return scaleXY.doubleValue();
+    /**
+     * Return the Y scale factor.
+     * 
+     * @return
+     * @throws IOException
+     * @throws ImageOpException
+     */
+    public double getScaleY() throws IOException, ImageOpException {
+    	if (scaleY == null) {
+    		prepareScaleParams();
+    	}
+    	return scaleY.doubleValue();
     }
 
     /**
@@ -481,29 +642,8 @@
     public int getDw() throws IOException {
         //logger.debug("get_paramDW()");
         if (paramDW == null) {
-
             paramDW = getAsInt("dw");
             paramDH = getAsInt("dh");
-
-            if (paramDW == 0 && input != null) {
-                /*
-                 * calculate dw using aspect ratio of image area
-                 */
-                userImgArea = getUserImgArea();
-                double imgAspect = userImgArea.getWidth() / userImgArea.getHeight();
-                // round up to make sure we don't squeeze dh
-                paramDW = (int) Math.ceil(paramDH * imgAspect);
-                setValue("dw", paramDW);
-            } else if (paramDH == 0 && input != null) {
-                /*
-                 * calculate dh using aspect ratio of image area
-                 */
-            	userImgArea = getUserImgArea();
-                double imgAspect = userImgArea.getWidth() / userImgArea.getHeight();
-                // round up to make sure we don't squeeze dw
-                paramDH = (int) Math.ceil(paramDW / imgAspect);
-                setValue("dh", paramDH);
-            }
         }
         return paramDW;
     }
@@ -518,35 +658,14 @@
     public int getDh() throws IOException {
         //logger.debug("get_paramDH()");
         if (paramDH == null) {
-
             paramDW = getAsInt("dw");
             paramDH = getAsInt("dh");
-
-            if (paramDW == 0 && input != null) {
-                /*
-                 * calculate dw using aspect ratio of image area
-                 */
-                userImgArea = getUserImgArea();
-                double imgAspect = userImgArea.getWidth() / userImgArea.getHeight();
-                // round up to make sure we don't squeeze dh
-                paramDW = (int) Math.ceil(paramDH * imgAspect);
-                setValue("dw", paramDW);
-            } else if (paramDH == 0 && input != null) {
-                /*
-                 * calculate dh using aspect ratio of image area
-                 */
-                userImgArea = getUserImgArea();
-                double imgAspect = userImgArea.getWidth() / userImgArea.getHeight();
-                // round up to make sure we don't squeeze dw
-                paramDH = (int) Math.ceil(paramDW / imgAspect);
-                setValue("dh", paramDH);
-            }
         }
         return paramDH;
     }
 
     /**
-     * Returns the width of the image area.
+     * Returns the relative width of the image area.
      * Uses ww parameter.
      * 
      * @return
@@ -558,15 +677,15 @@
         	paramWW = getAsFloat("ww");
         	if (hasOption("pxarea")) {
         		// area in absolute pixels - convert to relative
-        		ImageSize imgSize = getHiresSize();
-        		paramWW = paramWW / imgSize.getWidth(); 
+        		hiresSize = getHiresSize();
+        		paramWW = paramWW / hiresSize.getWidth(); 
         	}
         }
         return paramWW;
     }
 
     /**
-     * Returns the height of the image area.
+     * Returns the relative height of the image area.
      * Uses wh parameter.
      * 
      * @return
@@ -578,15 +697,15 @@
         	paramWH = getAsFloat("wh");
         	if (hasOption("pxarea")) {
         		// area in absolute pixels - convert to relative
-        		ImageSize imgSize = getHiresSize();
-        		paramWH = paramWH / imgSize.getHeight(); 
+        		hiresSize = getHiresSize();
+        		paramWH = paramWH / hiresSize.getHeight(); 
         	}
         }
         return paramWH;
     }
 
     /**
-     * Returns the x-offset of the image area.
+     * Returns the relative x-offset of the image area.
      * Uses wx parameter.
      * 
      * @return
@@ -606,7 +725,7 @@
     }
 
     /**
-     * Returns the y-offset of the image area.
+     * Returns the relative y-offset of the image area.
      * Uses wy parameter.
      * 
      * @return
@@ -661,63 +780,39 @@
     }
 
     /**
-     * Returns the area of the source image that will be transformed into the
-     * destination image.
-     * 
-     * @return
-     * @throws IOException
-     */
-    public Rectangle2D getUserImgArea() throws IOException {
-        if (userImgArea == null) {
-            // size of the currently selected input image
-            ImageSize imgSize = getInput().getSize();
-            // 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);
-            userImgArea = new Rectangle2D.Double(areaX, areaY, areaWidth, areaHeight);
-        }
-        return userImgArea;
-    }
-
-    /**
      * Returns the maximal area of the source image that will be used.
      * 
-     * This was meant to correct for missing pixels outside the 
-     * userImgArea when rotating oblique angles but is not yet implemented.
-     * Currently returns userImgArea.
+     * This was meant to include extra pixels outside the 
+     * imgArea when rotating by oblique angles but is not yet implemented.
+     * Currently returns imgArea.
      * 
      * @return
      * @throws IOException
      * @throws ImageOpException
      */
-    public Rectangle2D getOuterUserImgArea() throws IOException, ImageOpException {
-        if (outerUserImgArea == null) {
-            outerUserImgArea = getUserImgArea();
+    public Rectangle2D getOuterImgArea() throws IOException, ImageOpException {
+        if (outerImgArea == null) {
+        	// calculate scale parameters
+        	prepareScaleParams();
+            // start with imgArea
+            outerImgArea = imgArea;
 
             // image size in pixels
             ImageSize imgSize = getInput().getSize();
             Rectangle2D imgBounds = new Rectangle2D.Double(0, 0, imgSize.getWidth(), imgSize.getHeight());
 
             // clip area at the image border
-            outerUserImgArea = outerUserImgArea.createIntersection(imgBounds);
+            outerImgArea = outerImgArea.createIntersection(imgBounds);
 
             // check image parameters sanity
-            scaleXY = getScaleXY();
-            if ((outerUserImgArea.getWidth() < 1) || (outerUserImgArea.getHeight() < 1)
-                    || (scaleXY * outerUserImgArea.getWidth() < 2) || (scaleXY * outerUserImgArea.getHeight() < 2)) {
+            if ((outerImgArea.getWidth() < 1) || (outerImgArea.getHeight() < 1)
+                    || (scaleX * outerImgArea.getWidth() < 2) || (scaleY * outerImgArea.getHeight() < 2)) {
                 logger.error("ERROR: invalid scale parameter set!");
-            	logger.debug("scaleXY="+scaleXY+" outerUserImgArea="+outerUserImgArea);
+            	logger.debug("scaleX="+scaleX+" scaleY="+scaleY+" outerImgArea="+outerImgArea);
                 throw new ImageOpException("Invalid scale parameter set!");
             }
         }
-        return outerUserImgArea;
+        return outerImgArea;
     }
 
     /**
@@ -767,10 +862,12 @@
      */
     public boolean isImageSendable() throws IOException {
         if (imageSendable == null) {
-            String mimeType = getMimeType();
+            String mimeType = getInputMimeType();
             imageSendable = (mimeType != null
             		// input image is browser compatible
                     && (mimeType.equals("image/jpeg") || mimeType.equals("image/png") || mimeType.equals("image/gif"))
+                    // no forced type conversion
+                    && !(hasOption("jpg") || hasOption("png"))
                     // no zooming
                     && !(getWx() > 0f || getWy() > 0f || getWw() < 1f || getWh() < 1f
                     // no other image operations
@@ -793,7 +890,7 @@
      */
     public boolean isTransformRequired() throws IOException {
         ImageSize is = getInput().getSize();
-        ImageSize ess = getExpectedSourceSize();
+        ImageSize ess = getMinSourceSize();
         // does the image require processing?
         if (isImageSendable()) {
         	// does the image require rescaling?
--- a/common/src/main/java/digilib/image/ImageWorker.java	Mon Oct 26 14:37:47 2015 +0100
+++ b/common/src/main/java/digilib/image/ImageWorker.java	Tue Oct 27 11:23:30 2015 +0100
@@ -78,19 +78,24 @@
         docuImage.setQuality(jobinfo.getScaleQual());
 
         // get area of interest and scale factor
-        Rectangle loadRect = jobinfo.getOuterUserImgArea().getBounds();
-        double scaleXY = jobinfo.getScaleXY();
+        jobinfo.prepareScaleParams();
+        Rectangle loadRect = jobinfo.getOuterImgArea().getBounds();
+        double scaleX = jobinfo.getScaleX();
+        double scaleY = jobinfo.getScaleY();
 
         if (stopNow) {
             logger.debug("ImageWorker stopping (after setup)");
             return null;
         }
-        /*
+		/*
          * load, crop and scale the image 
          */
         if (docuImage.isSubimageSupported()) {
-        	// use subimage loading if possible
-            logger.debug("Subimage: scale " + scaleXY + " = " + (1 / scaleXY));
+        	/*
+        	 * use subimage loading with subsampling
+        	 */
+        	double scaleXY = scaleX + scaleY / 2d;
+            logger.debug("Subimage: scale " + scaleX + ", " + scaleY);
             double subf = 1d;
             double subsamp = 1d;
             if (scaleXY < 1) {
@@ -102,8 +107,9 @@
                     subsamp = Math.floor(subf);
                 }
                 // correct scaling factor by subsampling factor
-                scaleXY *= subsamp;
-                logger.debug("Using subsampling: " + subsamp + " rest " + scaleXY);
+                scaleX *= subsamp;
+                scaleY *= subsamp;
+                logger.debug("Using subsampling: " + subsamp + " rest " + scaleX + ", " + scaleY);
             }
             // load region with subsampling
             docuImage.loadSubimage(jobinfo.getInput(), loadRect, (int) subsamp);
@@ -113,9 +119,12 @@
                 return null;
             }
             // and scale
-            docuImage.scale(scaleXY, scaleXY);
+            docuImage.scale(scaleX, scaleY);
+            
         } else {
-            // else load and crop the whole file
+            /*
+             * else load and crop the whole file
+             */
             docuImage.loadImage(jobinfo.getInput());
             if (stopNow) {
                 logger.debug("ImageWorker stopping (after loading)");
@@ -127,8 +136,9 @@
                 logger.debug("ImageWorker stopping (after cropping)");
                 return null;
             }
-            docuImage.scale(scaleXY, scaleXY);
+            docuImage.scale(scaleX, scaleY);
         }
+        
         if (stopNow) {
             logger.debug("ImageWorker stopping (after scaling)");
             return null;
--- a/servlet/pom.xml	Mon Oct 26 14:37:47 2015 +0100
+++ b/servlet/pom.xml	Tue Oct 27 11:23:30 2015 +0100
@@ -22,7 +22,7 @@
   	<dependency>
   		<groupId>javax.servlet</groupId>
   		<artifactId>servlet-api</artifactId>
-  		<version>2.3</version>
+  		<version>2.4</version>
   		<type>jar</type>
   		<scope>provided</scope>
   	</dependency>
--- a/servlet/src/main/java/digilib/conf/DigilibServletConfiguration.java	Mon Oct 26 14:37:47 2015 +0100
+++ b/servlet/src/main/java/digilib/conf/DigilibServletConfiguration.java	Tue Oct 27 11:23:30 2015 +0100
@@ -88,7 +88,7 @@
     public final Long webappStartTime = System.currentTimeMillis();
 
     public static String getClassVersion() {
-        return "2.3.0 srv";
+        return DigilibConfiguration.getClassVersion() + " srv";
     }
     
     /* non-static getVersion for Java inheritance */
--- a/servlet/src/main/java/digilib/servlet/ServletOps.java	Mon Oct 26 14:37:47 2015 +0100
+++ b/servlet/src/main/java/digilib/servlet/ServletOps.java	Tue Oct 27 11:23:30 2015 +0100
@@ -383,7 +383,8 @@
         } else if (url.endsWith("/")) {
             url = url.substring(0, url.lastIndexOf("/"));
         }
-        response.setContentType("application/json;charset=UTF-8");
+        response.setCharacterEncoding("UTF-8");
+        response.setContentType("application/json,application/ld+json");
         PrintWriter writer;
         try {
             writer = response.getWriter();
--- a/servlet2/src/main/java/digilib/servlet/Scaler.java	Mon Oct 26 14:37:47 2015 +0100
+++ b/servlet2/src/main/java/digilib/servlet/Scaler.java	Tue Oct 27 11:23:30 2015 +0100
@@ -61,7 +61,7 @@
     private static final long serialVersionUID = -5439198888139362735L;
 
     /** digilib servlet version (for all components) */
-    public static final String version = "2.3.1 noasync";
+    public static final String version = DigilibServletConfiguration.getClassVersion() + " noasync";
 
     /** servlet error codes */
     public static enum Error {
--- a/servlet3/src/main/java/digilib/conf/DigilibServlet3Configuration.java	Mon Oct 26 14:37:47 2015 +0100
+++ b/servlet3/src/main/java/digilib/conf/DigilibServlet3Configuration.java	Tue Oct 27 11:23:30 2015 +0100
@@ -42,7 +42,7 @@
 public class DigilibServlet3Configuration extends DigilibServletConfiguration {
 
     public static String getClassVersion() {
-        return "2.3.2 async";
+        return DigilibServletConfiguration.getClassVersion() + " async";
     }
 
     /* non-static getVersion for Java inheritance */
--- a/webapp/pom.xml	Mon Oct 26 14:37:47 2015 +0100
+++ b/webapp/pom.xml	Tue Oct 27 11:23:30 2015 +0100
@@ -63,7 +63,7 @@
 						<groupId>org.apache.maven.plugins</groupId>
 						<artifactId>maven-war-plugin</artifactId>
 						<configuration>
-							<webXml>${basedir}/src/main/webapp/WEB-INF/web-2.3.xml</webXml>
+							<webXml>${basedir}/src/main/webapp/WEB-INF/web-2.4.xml</webXml>
 							<classifier>srv2</classifier>
 						</configuration>
 					</plugin>
--- a/webapp/src/main/webapp/WEB-INF/web-2.3.xml	Mon Oct 26 14:37:47 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
- 
-<web-app>
-  <!-- General description of your web application -->
-  <display-name>
-        digilib
-  </display-name>
-  <description>
-        This is the web frontend of the Digital Document Library.
-  </description>
-  <!-- The Intialisation Listener -->
-  <listener>
-        <listener-class>
-            digilib.conf.DigilibServletConfiguration
-        </listener-class>
-  </listener>
-  <!-- The Scaler servlet -->
-  <servlet>
-        <servlet-name>
-            Scaler
-        </servlet-name>
-        <servlet-class>
-            digilib.servlet.Scaler
-        </servlet-class>
-        <!-- Load this servlet at server startup time -->
-        <load-on-startup>
-            5
-        </load-on-startup>
-  </servlet>
-  <!-- The mapping for the Scaler servlet -->
-  <servlet-mapping>
-        <servlet-name>
-            Scaler
-        </servlet-name>
-        <url-pattern>
-            /servlet/Scaler/*
-        </url-pattern>
-  </servlet-mapping>
-  <servlet-mapping>
-        <servlet-name>
-            Scaler
-        </servlet-name>
-        <url-pattern>
-            /Scaler/*
-        </url-pattern>
-  </servlet-mapping>
-</web-app>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webapp/src/main/webapp/WEB-INF/web-2.4.xml	Tue Oct 27 11:23:30 2015 +0100
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
+ 
+<web-app>
+  <!-- General description of your web application -->
+  <display-name>
+        digilib
+  </display-name>
+  <description>
+        This is the web frontend of the Digital Document Library.
+  </description>
+  <!-- The Intialisation Listener -->
+  <listener>
+        <listener-class>
+            digilib.conf.DigilibServletConfiguration
+        </listener-class>
+  </listener>
+  <!-- The Scaler servlet -->
+  <servlet>
+        <servlet-name>
+            Scaler
+        </servlet-name>
+        <servlet-class>
+            digilib.servlet.Scaler
+        </servlet-class>
+        <!-- Load this servlet at server startup time -->
+        <load-on-startup>
+            5
+        </load-on-startup>
+  </servlet>
+  <!-- The mapping for the Scaler servlet -->
+  <servlet-mapping>
+        <servlet-name>
+            Scaler
+        </servlet-name>
+        <url-pattern>
+            /servlet/Scaler/*
+        </url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+        <servlet-name>
+            Scaler
+        </servlet-name>
+        <url-pattern>
+            /Scaler/*
+        </url-pattern>
+  </servlet-mapping>
+</web-app>