view common/src/main/java/digilib/image/ImageJobDescription.java @ 1590:bd71cb53e1a3

fix rounding bug with mo=crop and mo=fill. added tests for crop and fill.
author robcast
date Wed, 08 Feb 2017 17:28:02 +0100
parents e9ad60c4fb0c
children 7031524fc6e9
line wrap: on
line source

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;

import org.apache.log4j.Logger;

import digilib.conf.DigilibConfiguration;
import digilib.conf.DigilibRequest;
import digilib.image.DocuImage.ColorOp;
import digilib.io.DocuDirCache;
import digilib.io.DocuDirectory;
import digilib.io.FileOpException;
import digilib.io.FileOps;
import digilib.io.ImageInput;
import digilib.io.ImageSet;
import digilib.util.ImageSize;
import digilib.util.OptionsSet;
import digilib.util.Parameter;
import digilib.util.ParameterMap;

/**
 * A class for storing the set of parameters necessary for scaling images
 * with an ImageWorker.
 * 
 * This contains the functionality formerly found in Scaler.processRequest(),
 * only factorized.
 * 
 * @author cmielack, casties
 * 
 */

public class ImageJobDescription extends ParameterMap {

    protected DigilibConfiguration dlConfig = null;
    protected static Logger logger = Logger.getLogger("digilib.servlet");

    /* 
     * variables for caching values
     */
    protected ImageInput input = null;
    protected ImageSet imageSet = null;
    protected DocuDirectory fileDir = null;
    protected DocuImage docuImage = null;
    protected String filePath = 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;
    protected Integer paramDH = null;
    protected Float paramWX = null;
    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;

    /**
     * create empty ImageJobDescription.
     * 
     * @param dlcfg
     */
    public ImageJobDescription(DigilibConfiguration dlcfg) {
        super(30);
        initParams();
        dlConfig = dlcfg;
        dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
    }

    /**
     * set up Parameters
     * 
     * @see digilib.util.ParameterMap#initParams()
     */
    @Override
    protected void initParams() {
        // url of the page/document (second part)
        newParameter("fn", "", null, 's');
        // page number
        newParameter("pn", new Integer(1), null, 's');
        // width of client in pixels
        newParameter("dw", new Integer(0), null, 's');
        // height of client in pixels
        newParameter("dh", new Integer(0), null, 's');
        // left edge of image (float from 0 to 1)
        newParameter("wx", new Float(0), null, 's');
        // top edge in image (float from 0 to 1)
        newParameter("wy", new Float(0), null, 's');
        // width of image (float from 0 to 1)
        newParameter("ww", new Float(1), null, 's');
        // height of image (float from 0 to 1)
        newParameter("wh", new Float(1), null, 's');
        // scale factor
        newParameter("ws", new Float(1), null, 's');
        // special options like 'fit' for gifs
        newParameter("mo", this.options, null, 's');
        // rotation angle (degree)
        newParameter("rot", new Float(0), null, 's');
        // contrast enhancement factor
        newParameter("cont", new Float(0), null, 's');
        // brightness enhancement factor
        newParameter("brgt", new Float(0), null, 's');
        // color multiplicative factors
        newParameter("rgbm", "0/0/0", null, 's');
        // color additive factors
        newParameter("rgba", "0/0/0", null, 's');
        // display dpi resolution (total)
        newParameter("ddpi", new Float(0), null, 's');
        // display dpi X resolution
        newParameter("ddpix", new Float(0), null, 's');
        // display dpi Y resolution
        newParameter("ddpiy", new Float(0), null, 's');
        // scale factor for mo=ascale
        newParameter("scale", new Float(1), null, 's');
        // color conversion operation
        newParameter("colop", "", null, 's');
    }

    /*
     * (non-Javadoc)
     * 
     * @see digilib.servlet.ParameterMap#initOptions()
     */
    @Override
    protected void initOptions() {
        String s = this.getAsString("mo");
        options = new OptionsSet(s);
    }

    /**
     * Creates new ImageJobDescription by merging Parameters from a
     * DigilibRequest.
     * 
     * @param dlReq
     * @param dlcfg
     * @return
     * @throws ImageOpException 
     * @throws IOException 
     */
    public static ImageJobDescription getInstance(DigilibRequest dlReq, DigilibConfiguration dlcfg) throws IOException, ImageOpException {
        ImageJobDescription newMap = new ImageJobDescription(dlcfg);
        // 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;
    }

    /**
     * Creates new ImageJobDescription by merging Parameters from a
     * DigilibRequest and adding an ImageSet.
     * 
     * @param dlReq
     * @param imgs
     * @param dlcfg
     * @return
     * @throws ImageOpException 
     * @throws IOException 
     */
    public static ImageJobDescription getInstanceWithImgs(DigilibRequest dlReq, ImageSet imgs, DigilibConfiguration dlcfg) 
            throws IOException, ImageOpException {
        ImageJobDescription newMap = new ImageJobDescription(dlcfg);
        // add all params to this map
        newMap.params.putAll(dlReq.getParams());
        newMap.initOptions();
        newMap.setImageSet(imgs);
        newMap.prepareScaleParams();
        // add ImageJobDescription back into DigilibRequest
        dlReq.setJobDescription(newMap);
        return newMap;
    }

    /**
     * Creates new ImageJobDescription by merging Parameters from another
     * ParameterMap.
     * 
     * @param pm
     * @param dlcfg
     * @return
     * @throws ImageOpException 
     * @throws IOException 
     */
    public static ImageJobDescription getInstance(ParameterMap pm, DigilibConfiguration dlcfg) throws IOException, ImageOpException {
        ImageJobDescription newMap = new ImageJobDescription(dlcfg);
        // add all params to this map
        newMap.params.putAll(pm.getParams());
        newMap.initOptions();
        newMap.prepareScaleParams();
        return newMap;
    }

    
    /**
     * Prepare image scaling factors and coordinates.
     * 
     * Should be called by getInstance().
     * 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()");

        /*
         * 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 = prepareClipToFit();

        } 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 (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;
        if (scaleX == 0) {
            // dw undefined
            scaleX = scaleY;
        } else if (scaleY == 0) {
            // dh undefined
            scaleY = scaleX;
        } else if (hasOption("crop")) {
            // use the bigger factor to get fill-the-box
            if (scaleX > scaleY) {
                scaleY = scaleX;
                // crop mode uses whole destination rect
                long croppedAreaHeight = Math.round(getDh() / scaleY);
                if (areaHeight > croppedAreaHeight) {
                	// center cropped area
                	areaY += (areaHeight - croppedAreaHeight) / 2;
                }
                areaHeight = croppedAreaHeight;
                // re-compute scaleY
                scaleY = getDh() / (double) areaHeight;
            } else {
                scaleX = scaleY;
                // crop mode uses whole destination rect
                long croppedAreaWidth = Math.round(getDw() / scaleX);
                if (areaWidth > croppedAreaWidth) {
                	// center cropped area
                	areaX += (areaWidth - croppedAreaWidth) / 2;
                }
                areaWidth = croppedAreaWidth;
                // re-compute scaleX
                scaleX = getDw() / (double) areaWidth;
            }
        } else {
            // use the smaller factor to get fit-in-box
            if (scaleX > scaleY) {
                scaleX = scaleY;
                if (hasOption("fill")) {
                    // fill mode uses whole destination rect
                    long filledAreaWidth = Math.round(getDw() / scaleX);
                    if (filledAreaWidth > areaWidth) {
                    	// center filled area
                    	areaX -= (filledAreaWidth - areaWidth) / 2;
                    }
                    areaWidth = filledAreaWidth;
                    // re-compute scaleX
                    scaleX = getDw() / (double) areaWidth;
                }
            } else {
                scaleY = scaleX;
                if (hasOption("fill")) {
                    // fill mode uses whole destination rect
                    long filledAreaHeight = Math.round(getDh() / scaleY);
                    if (filledAreaHeight > areaHeight) {
                    	// center filled area
                    	areaY -= (filledAreaHeight - areaHeight) / 2;
                    }
                    areaHeight = filledAreaHeight;
                    // re-compute scaleY
                    scaleY = getDh() / (double) areaHeight;
                }
            }
        }
        
        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 to fill destination size changing aspect ratio.
     * 
     * 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")) {
            /*
             * 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 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);
    }


    /**
     * Clip to fit: don't scale.
     * 
     * Sets ScaleX and ScaleY to 1.0.
     */
    protected Rectangle2D prepareClipToFit() 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
     */
    public String getInputMimeType() throws IOException {
        if (mimeType == null) {
            input = getInput();
            mimeType = input.getMimetype();
        }
        return mimeType;
    }


    /**
     * Return the mime-type of the output.
     * 
     * @return
     */
    public String getOutputMimeType() {
        // forced destination image type
        if (hasOption("jpg")) {
            return "image/jpeg";
        } else if (hasOption("png")) {
            return "image/png";
        }
        // use input image type
        try {
            String mt = getInputMimeType();
            if ((mt.equals("image/jpeg") || mt.equals("image/jp2") || mt.equals("image/fpx"))) {
                return "image/jpeg";
            } else {
                return "image/png";
            }
        } catch (IOException e) {
            logger.error("No input when trying to getOutputMimeType!");
        }
        return null;
    }
    
    /**
     * Set the current ImageInput.
     * 
     * @param input
     *            the input to set
     */
    public void setInput(ImageInput input) {
        this.input = input;
        // create and set ImageSet if needed
        if (dirCache == null && imageSet == null) {
            imageSet = new ImageSet();
            imageSet.add(input);
        }
    }

    /**
     * Returns the ImageInput to use.
     * 
     * Note: uses getMinSourceSize().
     * 
     * @return
     * @throws IOException
     */
    public ImageInput getInput() throws IOException {
        if (input == null) {
            imageSet = getImageSet();

            /* select a resolution */
            if (isHiresOnly()) {
                // get first element (= highest resolution)
                input = imageSet.getBiggest();
            } else if (isLoresOnly()) {
                // enforced lores uses next smaller resolution
                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(getMinSourceSize());
                if (input == null) {
                    // this is the highest we have
                    input = imageSet.getBiggest();
                }
            }
            if (input == null || input.getMimetype() == null) {
                throw new FileOpException("Unable to load " + input);
            }
            logger.info("Planning to load: " + input);
        }
        return input;
    }

    /**
     * Return the DocuDirectory for the input (file).
     * 
     * @return
     * @throws FileOpException
     */
    public DocuDirectory getFileDirectory() throws FileOpException {
        if (fileDir == null) {
            String fp = getFilePath();
            fileDir = dirCache.getDirectory(fp);
            if (fileDir == null) {
                throw new FileOpException("Directory " + getFilePath() + " not found.");
            }
        }
        return fileDir;
    }

    /**
     * Return the ImageSet to load.
     * 
     * @return
     * @throws FileOpException
     */
    public ImageSet getImageSet() throws FileOpException {
        if (imageSet == null) {
            if (dirCache == null) {
                throw new FileOpException("No DirCache configured!");
            }
            imageSet = (ImageSet) dirCache.getFile(getFilePath(), getAsInt("pn"));
            if (imageSet == null) {
                throw new FileOpException("File " + getFilePath() + "(" + getAsInt("pn") + ") not found.");
            }
        }
        return imageSet;
    }

    /**
     * Set the current ImageSet.
     * 
     * @param imageSet
     */
    public void setImageSet(ImageSet imageSet) {
        this.imageSet = imageSet;
    }
    
    
    /**
     * Return the file path name from the request.
     * 
     * @return
     */
    public String getFilePath() {
        if (filePath == null) {
            String s = this.getAsString("request.path");
            s += this.getAsString("fn");
            filePath = FileOps.normalName(s);
        }
        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("fit") || 
        		!(hasOption("clip") || hasOption("osize") || hasOption("ascale") || hasOption("squeeze"));
    }

    /**
     * Do not scale, just crop original resolution.
     * 
     * @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");
    }

    /**
     * Return the minimum size the source image should have for scaling.
     * 
     * 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) {
            // 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;
    }

    /**
     * Return the size of the highest resolution image.
     * 
     * @return
     * @throws IOException
     */
    public ImageSize getHiresSize() throws IOException {
        //logger.debug("get_hiresSize()");
        if (hiresSize == null) {
	        ImageSet fileset = getImageSet();
	        ImageInput hiresFile = fileset.getBiggest();
	        hiresSize = hiresFile.getSize();
	        if (hiresSize == null) {
	        	throw new FileOpException("Can't get size from hires image file!");
	        }
        }
        return hiresSize;
    }

    /**
     * Return the size of the selected input image.
     * 
     * Note: may use getMinSourceSize().
     * 
     * @return
     * @throws IOException
     */
    public ImageSize getImgSize() throws IOException {
        //logger.debug("get_hiresSize()");
        if (imgSize == null) {
        	imgSize = getInput().getSize();
        }
        return imgSize;
    }

    /**
     * Set the image size.
     * 
     * @param size
     */
    public void setImgSize(ImageSize size) {
        this.imgSize = size;
    }
      
    /**
     * Return the X scale factor.
     * 
     * @return
     * @throws IOException
     * @throws ImageOpException
     */
    public double getScaleX() throws IOException, ImageOpException {
    	if (scaleX == null) {
    		prepareScaleParams();
    	}
    	return scaleX.doubleValue();
    }

    /**
     * Return the Y scale factor.
     * 
     * @return
     * @throws IOException
     * @throws ImageOpException
     */
    public double getScaleY() throws IOException, ImageOpException {
    	if (scaleY == null) {
    		prepareScaleParams();
    	}
    	return scaleY.doubleValue();
    }

    /**
     * Return the width of the destination image.
     * Uses dw parameter.
     * 
     * @return
     */
    public int getDw() {
        //logger.debug("get_paramDW()");
        if (paramDW == null) {
            paramDW = getAsInt("dw");
        }
        return paramDW;
    }

    /**
     * Return the height of the destination image.
     * Uses dh parameter.
     * 
     * @return
     */
    public int getDh() {
        //logger.debug("get_paramDH()");
        if (paramDH == null) {
            paramDH = getAsInt("dh");
        }
        return paramDH;
    }

    /**
     * Return the relative width of the image area.
     * Uses ww parameter.
     * Converts ww in pixels to relative. 
     * 
     * @return
     * @throws IOException
     */
    public Float getWw() throws IOException {
        //logger.debug("get_paramWW()");
        if (paramWW == null) {
        	paramWW = getAsFloat("ww");
        	if (hasOption("pxarea")) {
        		// area in absolute pixels - convert to relative
        		hiresSize = getHiresSize();
        		paramWW = paramWW / hiresSize.getWidth(); 
        	}
        }
        return paramWW;
    }

    /**
     * Return the relative height of the image area.
     * Uses wh parameter.
     * Converts wh in pixels to relative. 
     * 
     * @return
     * @throws IOException
     */
    public Float getWh() throws IOException {
        //logger.debug("get_paramWH()");
        if (paramWH == null) {
        	paramWH = getAsFloat("wh");
        	if (hasOption("pxarea")) {
        		// area in absolute pixels - convert to relative
        		hiresSize = getHiresSize();
        		paramWH = paramWH / hiresSize.getHeight(); 
        	}
        }
        return paramWH;
    }

    /**
     * Return the relative x-offset of the image area.
     * Uses wx parameter.
     * Converts wx in pixels to relative. 
     * 
     * @return
     * @throws IOException
     */
    public Float getWx() throws IOException {
        //logger.debug("get_paramWX()");
        if (paramWX == null) {
        	paramWX = getAsFloat("wx");
        	if (hasOption("pxarea")) {
        		// area in absolute pixels - convert to relative
        		ImageSize imgSize = getHiresSize();
        		paramWX = paramWX / imgSize.getWidth(); 
        	}
        }
        return paramWX;
    }

    /**
     * Return the relative y-offset of the image area.
     * Uses wy parameter.
     * Converts wy in pixels to relative. 
     * 
     * @return
     * @throws IOException
     */
    public Float getWy() throws IOException {
        //logger.debug("get_paramWY()");
        if (paramWY == null) {
        	paramWY = getAsFloat("wy");
        	if (hasOption("pxarea")) {
        		// area in absolute pixels - convert to relative
        		ImageSize imgSize = getHiresSize();
        		paramWY = paramWY / imgSize.getHeight(); 
        	}
        }
        return paramWY;
    }

    /**
     * Return image quality as an integer.
     * 
     * @return
     */
    public int getScaleQual() {
        //logger.debug("get_scaleQual()");
        int qual = dlConfig.getAsInt("default-quality");
        if (hasOption("q0"))
            qual = 0;
        else if (hasOption("q1"))
            qual = 1;
        else if (hasOption("q2")) 
            qual = 2;
        return qual;
    }

    /**
     * Return the color operation as a ColorOp.
     * 
     * @return
     */
    public ColorOp getColOp() {
        String op = getAsString("colop");
        if (op == null || op.length() == 0) {
            return null;
        }
        try {
            return ColorOp.valueOf(op.toUpperCase());
        } catch (Exception e) {
            logger.error("Invalid color op: " + op);
        }
        return null;
    }

    /**
     * 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.
     * Currently returns imgArea.
     * 
     * @return
     * @throws IOException
     * @throws ImageOpException
     */
    public Rectangle2D getOuterImgArea() throws IOException, ImageOpException {
        if (outerImgArea == null) {
        	// calculate scale parameters
        	if (imgArea == null) {
        		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
            outerImgArea = outerImgArea.createIntersection(imgBounds);

            // check image parameters sanity
            if ((outerImgArea.getWidth() < 1) || (outerImgArea.getHeight() < 1)
                    || (scaleX * outerImgArea.getWidth() < 2) || (scaleY * outerImgArea.getHeight() < 2)) {
                logger.error("ERROR: invalid scale parameter set!");
            	logger.debug("scaleX="+scaleX+" scaleY="+scaleY+" outerImgArea="+outerImgArea);
                throw new ImageOpException("Invalid scale parameter set!");
            }
        }
        return outerImgArea;
    }

    /**
     * Get the RGBM parameter set.
     * 
     * @return
     */
    public float[] getRGBM() {
        if (paramRGBM == null) {
            Parameter p = params.get("rgbm");
            if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
                paramRGBM = p.parseAsFloatArray("/");
            }
        }
        return paramRGBM;
    }

    /**
     * Get the RGBA parameter set.
     * 
     * @return
     */
    public float[] getRGBA() {
        if (paramRGBA == null) {
            Parameter p = params.get("rgba");
            if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
                paramRGBA = p.parseAsFloatArray("/");
            }
        }
        return paramRGBA;
    }

    /**
     * Has send-as-file been requested?
     * 
     * @return
     */
    public boolean getSendAsFile() {
        return hasOption("file") || hasOption("rawfile");
    }

    /**
     * Returns if the image can be sent without processing. Takes image type and
     * additional image operations into account. Does not check requested size
     * transformation.
     * 
     * @return
     * @throws IOException
     */
    public boolean isImageSendable() throws IOException {
        if (imageSendable == null) {
            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
                    || hasOption("vmir") || hasOption("hmir")
                    || (getAsFloat("rot") != 0.0)
                    || (getRGBM() != null)
                    || (getRGBA() != null)
                    || (this.getColOp() != null) 
                    || (getAsFloat("cont") != 0.0) || (getAsFloat("brgt") != 0.0)));
        }
        return imageSendable;
    }

    /**
     * Returns if any transformation of the source image (image manipulation or
     * format conversion) is required.
     * 
     * @return
     * @throws IOException
     */
    public boolean isTransformRequired() throws IOException {
        ImageSize is = getInput().getSize();
        ImageSize ess = getMinSourceSize();
        // does the image require processing?
        if (isImageSendable()) {
        	// does the image require rescaling?
        	if (isLoresOnly() && is.isSmallerThan(ess)) {
        		// lores: send even if smaller
        		return false;
        	} else if (is.fitsIn(ess)) {
        		// TODO: check condition again. had && !(isLoresOnly() || isHiresOnly())
        		// send if it fits
        		return false;
        	}
        }
        return true;
    }

    /**
     * @return the docuImage
     */
    public DocuImage getDocuImage() {
        return docuImage;
    }

    /**
     * Set the current docuImage.
     * 
     * @param docuImage
     *            the docuImage to set
     */
    public void setDocuImage(DocuImage docuImage) {
        this.docuImage = docuImage;
    }
}