view servlet/src/digilib/servlet/Scaler.java @ 298:1ecaf9c1fd8a

Servlet version 1.5.0b -- the beginning of the next generation :-) - code restructuring to improve scaleability - new Initialiser servlet that must be run first - image transformation work moved to DigilibImageWorker class - Maximum number of concurrent threads limited by Semaphore - old JIMI toolkit implementation removed
author robcast
date Sun, 24 Oct 2004 20:23:50 +0200
parents ffafe3e470fb
children 9db80eb0f50b
line wrap: on
line source

/*
 * Scaler -- Scaler servlet main class
 * 
 * Digital Image Library servlet components
 * 
 * Copyright (C) 2001, 2002, 2003 Robert Casties (robcast@mail.berlios.de)
 * 
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 * 
 * Please read license.txt for the full details. A copy of the GPL may be found
 * at http://www.gnu.org/copyleft/lgpl.html
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA 02111-1307 USA
 *  
 */

package digilib.servlet;

import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.util.List;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import digilib.auth.AuthOpException;
import digilib.auth.AuthOps;
import digilib.image.ImageOpException;
import digilib.image.ImageOps;
import digilib.image.ImageSize;
import digilib.io.DocuDirCache;
import digilib.io.FileOpException;
import digilib.io.FileOps;
import digilib.io.ImageFile;
import digilib.io.ImageFileset;

//import tilecachetool.*;

/**
 * @author casties
 */
//public class Scaler extends HttpServlet implements SingleThreadModel {
public class Scaler extends HttpServlet {

	private static final long serialVersionUID = -325080527268912852L;

	/** digilib servlet version (for all components) */
	public static final String dlVersion = "1.5.0b";

	/** logger for accounting requests */
	private static Logger accountlog = Logger.getLogger("account.request");
	
	/** gengeral logger for this class */
	private static Logger logger = Logger.getLogger("digilib.servlet");
	
	/** logger for authentication related */
	private static Logger authlog = Logger.getLogger("digilib.auth");

	/** general error code */
	public static final int ERROR_UNKNOWN = 0;

	/** error code for authentication error */
	public static final int ERROR_AUTH = 1;

	/** error code for file operation error */
	public static final int ERROR_FILE = 2;

	/** error code for image operation error */
	public static final int ERROR_IMAGE = 3;

	/** DocuDirCache instance */
	DocuDirCache dirCache;

	/** authentication error image file */
	File denyImgFile;

	/** image error image file */
	File errorImgFile;

	/** subsampling before scaling */
	float minSubsample = 2f;

	/** send files as is? */
	boolean sendFileAllowed = true;

	/** default scaling quality */
	int defaultQuality = 1;

	/** DigilibConfiguration instance */
	DigilibConfiguration dlConfig;

	/** use authorization database */
	boolean useAuthorization = true;

	/** AuthOps instance */
	AuthOps authOp;

	// EXPRIMENTAL
	/** try to enlarge cropping area for "oblique" angles */
	boolean wholeRotArea = false;

	/**
	 * Initialisation on first run.
	 * 
	 * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
	 */
	public void init(ServletConfig config) throws ServletException {
		super.init(config);

		System.out
				.println("***** Digital Image Library Image Scaler Servlet (version "
						+ dlVersion + ") *****");
		// say hello in the log file
		logger
				.info("***** Digital Image Library Image Scaler Servlet (version "
						+ dlVersion + ") *****");

		// get our ServletContext
		ServletContext context = config.getServletContext();
		// see if there is a Configuration instance
		dlConfig = (DigilibConfiguration) context
				.getAttribute("digilib.servlet.configuration");
		if (dlConfig == null) {
			// no Configuration
			throw new ServletException("No Configuration!");
			}
		// set our AuthOps
		useAuthorization = dlConfig.getAsBoolean("use-authorization");
		authOp = (AuthOps) dlConfig.getValue("servlet.auth.op");

		// DocuDirCache instance
		dirCache = (DocuDirCache) dlConfig.getValue("servlet.dir.cache");
		denyImgFile = (File) dlConfig.getValue("denied-image");
		errorImgFile = (File) dlConfig.getValue("error-image");
		sendFileAllowed = dlConfig.getAsBoolean("sendfile-allowed");
		minSubsample = dlConfig.getAsFloat("subsample-minimum");
		defaultQuality = dlConfig.getAsInt("default-quality");
	}

	/** Process the HTTP Get request */
	public void doGet(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
		accountlog.info("GET from "+request.getRemoteAddr());
		// create new request with defaults
		DigilibRequest dlReq = new DigilibRequest();
		// set with request parameters
		dlReq.setWithRequest(request);
		// add DigilibRequest to ServletRequest
		request.setAttribute("digilib.servlet.request", dlReq);
		// do the processing
		processRequest(request, response);
	}

	/** Process the HTTP Post request */
	public void doPost(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
		accountlog.info("POST from "+request.getRemoteAddr());
		// create new request with defaults
		DigilibRequest dlReq = new DigilibRequest();
		// set with request parameters
		dlReq.setWithRequest(request);
		// add DigilibRequest to ServletRequest
		request.setAttribute("digilib.servlet.request", dlReq);
		// do the processing
		processRequest(request, response);
	}

	/** main request handler. */
	void processRequest(HttpServletRequest request, HttpServletResponse response)
			throws ServletException {

		if (dlConfig == null) {
			throw new ServletException("ERROR: No Configuration!");
		}

		accountlog.debug("request: "+request.getQueryString());
		logger.debug("request: "+request.getQueryString());
		
		// time for benchmarking
		long startTime = System.currentTimeMillis();
		// output mime/type
		String mimeType = "image/png";

		/* parameters for a session */

		// scale the image file to fit window size i.e. respect dw,dh
		boolean scaleToFit = true;
		// scale the image by a fixed factor only
		boolean absoluteScale = false;
		// only crop the image to fit
		boolean cropToFit = false;
		// send the file as is
		boolean sendFile = false;
		// use low resolution images only
		boolean loresOnly = false;
		// use hires images only
		boolean hiresOnly = false;
		// interpolation to use for scaling
		int scaleQual = defaultQuality;
		// send html error message (or image file)
		boolean errorMsgHtml = false;
		// mirror the image
		boolean doMirror = false;
		// angle of mirror axis
		float mirrorAngle = 0;
		// original (hires) image resolution
		float origResX = 0;
		float origResY = 0;

		/* request parameters */

		DigilibRequest dlRequest = (DigilibRequest) request
				.getAttribute("digilib.servlet.request");

		// destination image width
		int paramDW = dlRequest.getAsInt("dw");
		// destination image height
		int paramDH = dlRequest.getAsInt("dh");
		// relative area x_offset (0..1)
		float paramWX = dlRequest.getAsFloat("wx");
		// relative area y_offset
		float paramWY = dlRequest.getAsFloat("wy");
		// relative area width (0..1)
		float paramWW = dlRequest.getAsFloat("ww");
		// relative area height
		float paramWH = dlRequest.getAsFloat("wh");
		// scale factor (additional to dw/width, dh/height)
		float paramWS = dlRequest.getAsFloat("ws");
		// rotation angle
		float paramROT = dlRequest.getAsFloat("rot");
		// contrast enhancement
		float paramCONT = dlRequest.getAsFloat("cont");
		// brightness enhancement
		float paramBRGT = dlRequest.getAsFloat("brgt");
		// color modification
		float[] paramRGBM = null;
		Parameter p = dlRequest.get("rgbm");
		if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
			paramRGBM = p.parseAsFloatArray("/");
		}
		float[] paramRGBA = null;
		p = dlRequest.get("rgba");
		if (p.hasValue() && (!p.getAsString().equals("0/0/0"))) {
			paramRGBA = p.parseAsFloatArray("/");
		}
		// destination resolution (DPI)
		float paramDDPIX = dlRequest.getAsFloat("ddpix");
		float paramDDPIY = dlRequest.getAsFloat("ddpiy");
		if ((paramDDPIX == 0) || (paramDDPIY == 0)) {
			// if X or Y resolution isn't set, use DDPI
			paramDDPIX = dlRequest.getAsFloat("ddpi");
			paramDDPIY = paramDDPIX;
		}

		/*
		 * operation mode: "fit": always fit to page, "clip": send original
		 * resolution cropped, "file": send whole file (if allowed)
		 */
		if (dlRequest.hasOption("mo", "clip")) {
			scaleToFit = false;
			absoluteScale = false;
			cropToFit = true;
			sendFile = false;
			hiresOnly = true;
		} else if (dlRequest.hasOption("mo", "fit")) {
			scaleToFit = true;
			absoluteScale = false;
			cropToFit = false;
			sendFile = false;
			hiresOnly = false;
		} else if (dlRequest.hasOption("mo", "osize")) {
			scaleToFit = false;
			absoluteScale = true;
			cropToFit = false;
			sendFile = false;
			hiresOnly = true;
		}
		// operation mode: "lores": try to use scaled image, "hires": use
		// unscaled image
		//   "autores": try best fitting resolution
		if (dlRequest.hasOption("mo", "lores")) {
			loresOnly = true;
			hiresOnly = false;
		} else if (dlRequest.hasOption("mo", "hires")) {
			loresOnly = false;
			hiresOnly = true;
		} else if (dlRequest.hasOption("mo", "autores")) {
			loresOnly = false;
			hiresOnly = false;
		}
		// operation mode: "errtxt": error message in html, "errimg": error
		// image
		if (dlRequest.hasOption("mo", "errtxt")) {
			errorMsgHtml = true;
		} else if (dlRequest.hasOption("mo", "errimg")) {
			errorMsgHtml = false;
		}
		// operation mode: "q0" - "q2": interpolation quality
		if (dlRequest.hasOption("mo", "q0")) {
			scaleQual = 0;
		} else if (dlRequest.hasOption("mo", "q1")) {
			scaleQual = 1;
		} else if (dlRequest.hasOption("mo", "q2")) {
			scaleQual = 2;
		}

		// check with the maximum allowed size (if set)
		int maxImgSize = dlConfig.getAsInt("max-image-size");
		if (maxImgSize > 0) {
			paramDW = (paramDW * paramWS > maxImgSize) ? (int) (maxImgSize / paramWS)
					: paramDW;
			paramDH = (paramDH * paramWS > maxImgSize) ? (int) (maxImgSize / paramWS)
					: paramDH;
		}
		
		//"big" try for all file/image actions
		try {

			// ImageFileset of the image to load
			ImageFileset fileset = null;

			/* find the file to load/send */

			// get PathInfo
			String loadPathName = dlRequest.getFilePath();

			/* check permissions */
			if (useAuthorization) {
				// get a list of required roles (empty if no restrictions)
				List rolesRequired = authOp.rolesForPath(loadPathName, request);
				if (rolesRequired != null) {
					authlog.debug("Role required: " + rolesRequired);
					authlog.debug("User: " + request.getRemoteUser());
					// is the current request/user authorized?
					if (!authOp.isRoleAuthorized(rolesRequired, request)) {
						// send deny answer and abort
						throw new AuthOpException();
						}
					}
				}

			// find the file(set)
			ImageFile fileToLoad;
			fileset = (ImageFileset) dirCache.getFile(loadPathName, dlRequest
					.getAsInt("pn"), FileOps.CLASS_IMAGE);
			if (fileset == null) {
				throw new FileOpException("File " + loadPathName + "("
						+ dlRequest.getAsInt("pn") + ") not found.");
			}

			/* calculate expected source image size */
			ImageSize expectedSourceSize = new ImageSize();
			if (scaleToFit) {
				float scale = (1 / Math.min(paramWW, paramWH)) * paramWS;
				expectedSourceSize.setSize((int) (paramDW * scale),
					(int) (paramDH * scale));
			} else {
				expectedSourceSize.setSize((int) (paramDW * paramWS),
					(int) (paramDH * paramWS));
			}

			/* select a resolution */
			if (hiresOnly) {
				// get first element (= highest resolution)
				fileToLoad = fileset.getBiggest();
			} else if (loresOnly) {
				// enforced lores uses next smaller resolution
				fileToLoad = fileset.getNextSmaller(expectedSourceSize);
				if (fileToLoad == null) {
					// this is the smallest we have
					fileToLoad = fileset.getSmallest();
				}
			} else {
				// autores: use next higher resolution
				fileToLoad = fileset.getNextBigger(expectedSourceSize);
				if (fileToLoad == null) {
					// this is the highest we have
					fileToLoad = fileset.getBiggest();
				}
			}
			logger.info("Planning to load: " + fileToLoad.getFile());

			/*
			 * send the image if its mo=(raw)file
			 */
			if (dlRequest.hasOption("mo", "file")
				|| dlRequest.hasOption("mo", "rawfile")) {
				if (sendFileAllowed) {
					String mt = null;
					if (dlRequest.hasOption("mo", "rawfile")) {
						mt = "application/octet-stream";
					}
					logger.debug("Sending RAW File as is.");
					ServletOps.sendFile(fileToLoad.getFile(), mt, response);
					logger.info("Done in "
							+ (System.currentTimeMillis() - startTime) + "ms");
					return;
				}
			}

			/*
			 * prepare resolution for original size
			 */
			if (absoluteScale) {
				// get original resolution from metadata
				fileset.checkMeta();
				origResX = fileset.getResX();
				origResY = fileset.getResY();
				if ((origResX == 0) || (origResY == 0)) {
					throw new ImageOpException("Missing image DPI information!");
				}

				if ((paramDDPIX == 0) || (paramDDPIY == 0)) {
					throw new ImageOpException(
							"Missing display DPI information!");
				}
			}

			// check the source image
			if (!fileToLoad.isChecked()) {
				ImageOps.checkFile(fileToLoad);
			}
			// get the source image type
			mimeType = fileToLoad.getMimetype();
			// get the source image size
			ImageSize imgSize = fileToLoad.getSize();

			// decide if the image can be sent as is
			boolean mimetypeSendable = mimeType.equals("image/jpeg")
					|| mimeType.equals("image/png")
					|| mimeType.equals("image/gif");
			boolean imagoOptions = dlRequest.hasOption("mo", "hmir")
					|| dlRequest.hasOption("mo", "vmir") || (paramROT != 0)
					|| (paramRGBM != null) || (paramRGBA != null)
					|| (paramCONT != 0) || (paramBRGT != 0);
			boolean imageSendable = mimetypeSendable && !imagoOptions;

			/*
			 * if not autoRes and image smaller than requested size then send as
			 * is. if autoRes and image has requested size then send as is. if
			 * not autoScale and not scaleToFit nor cropToFit then send as is
			 * (mo=file)
			 */
			if (imageSendable
					&& ((loresOnly && fileToLoad.getSize().isSmallerThan(
							expectedSourceSize)) || (!(loresOnly || hiresOnly) && fileToLoad
							.getSize().fitsIn(expectedSourceSize)))) {

				logger.debug("Sending File as is.");

				ServletOps.sendFile(fileToLoad.getFile(), null, response);

				logger.info("Done in "
						+ (System.currentTimeMillis() - startTime) + "ms");
				return;
			}

			// set missing dw or dh from aspect ratio
			float imgAspect = fileToLoad.getAspect();
			if (paramDW == 0) {
				paramDW = (int) Math.round(paramDH * imgAspect);
			} else if (paramDH == 0) {
				paramDH = (int) Math.round(paramDW / imgAspect);
			}

			/* crop and scale the image */

			logger.debug("IMG: " + imgSize.getWidth() + "x"
					+ imgSize.getHeight());
			logger.debug("time " + (System.currentTimeMillis() - startTime)
					+ "ms");

			// coordinates and scaling
			float areaXoff;
			float areaYoff;
			float areaWidth;
			float areaHeight;
			float scaleX;
			float scaleY;
			float scaleXY;

			// coordinates using Java2D
			// image size in pixels
			Rectangle2D imgBounds = new Rectangle2D.Float(0, 0, imgSize
					.getWidth(), imgSize.getHeight());
			// user window area in [0,1] coordinates
			Rectangle2D relUserArea = new Rectangle2D.Float(paramWX, paramWY,
					paramWW, paramWH);
			// transform from relative [0,1] to image coordinates.
			AffineTransform imgTrafo = AffineTransform.getScaleInstance(imgSize
					.getWidth(), imgSize.getHeight());
			// transform user coordinate area to image coordinate area
			Rectangle2D userImgArea = imgTrafo.createTransformedShape(
					relUserArea).getBounds2D();

			// calculate scaling factors based on inner user area
			if (scaleToFit) {
				areaWidth = (float) userImgArea.getWidth();
				areaHeight = (float) userImgArea.getHeight();
				scaleX = paramDW / areaWidth * paramWS;
				scaleY = paramDH / areaHeight * paramWS;
				scaleXY = (scaleX > scaleY) ? scaleY : scaleX;
			} else if (absoluteScale) {
				// absolute scale
				scaleX = paramDDPIX / origResX;
				scaleY = paramDDPIY / origResY;
				// currently only same scale :-(
				scaleXY = scaleX;
				areaWidth = paramDW / scaleXY * paramWS;
				areaHeight = paramDH / scaleXY * paramWS;
				// reset user area size
				userImgArea.setRect(userImgArea.getX(), userImgArea.getY(),
						areaWidth, areaHeight);
			} else {
				// crop to fit
				areaWidth = paramDW * paramWS;
				areaHeight = paramDH * paramWS;
				// reset user area size
				userImgArea.setRect(userImgArea.getX(), userImgArea.getY(),
						areaWidth, areaHeight);
				scaleX = 1f;
				scaleY = 1f;
				scaleXY = 1f;
			}

			// enlarge image area for rotations to cover additional pixels
			Rectangle2D outerUserImgArea = userImgArea;
			Rectangle2D innerUserImgArea = userImgArea;
			if (wholeRotArea) {
				if (paramROT != 0) {
					try {
						// rotate user area coordinates around center of user
						// area
						AffineTransform rotTrafo = AffineTransform
								.getRotateInstance(Math.toRadians(paramROT),
										userImgArea.getCenterX(), userImgArea
												.getCenterY());
						// get bounds from rotated end position
						innerUserImgArea = rotTrafo.createTransformedShape(
								userImgArea).getBounds2D();
						// get bounds from back-rotated bounds
						outerUserImgArea = rotTrafo.createInverse()
								.createTransformedShape(innerUserImgArea)
								.getBounds2D();
					} catch (NoninvertibleTransformException e1) {
						// this shouldn't happen anyway
						logger.error(e1);
					}
				}
			}

			logger.debug("Scale " + scaleXY + "(" + scaleX + "," + scaleY
					+ ") on " + outerUserImgArea);

			// clip area at the image border
			outerUserImgArea = outerUserImgArea.createIntersection(imgBounds);

			// check image parameters sanity
			if ((outerUserImgArea.getWidth() < 1)
					|| (outerUserImgArea.getHeight() < 1)
					|| (scaleXY * outerUserImgArea.getWidth() < 2)
					|| (scaleXY * outerUserImgArea.getHeight() < 2)) {
				logger.error("ERROR: invalid scale parameter set!");
				throw new ImageOpException("Invalid scale parameter set!");
			}

			/*
			 * submit the image worker job
			 */

			DigilibWorker job = new DigilibImageWorker(dlConfig, response,
					mimeType, scaleQual, dlRequest, paramROT, paramCONT,
					paramBRGT, paramRGBM, paramRGBA, fileToLoad, scaleXY,
					outerUserImgArea, innerUserImgArea, minSubsample,
					wholeRotArea);

			job.run();
			if (job.hasError()) {
				throw new ImageOpException(job.getError().toString());
			}

			logger.debug("servlet done in "
					+ (System.currentTimeMillis() - startTime));

			/* error handling */

		} // end of "big" try
		catch (IOException e) {
			logger.error("ERROR: File IO Error: " + e);
			digilibError(errorMsgHtml, ERROR_FILE,
					"ERROR: File IO Error: " + e, response);
		} catch (AuthOpException e) {
			logger.error("ERROR: Authorization error: " + e);
			digilibError(errorMsgHtml, ERROR_AUTH,
					"ERROR: Authorization error: " + e, response);
		} catch (ImageOpException e) {
			logger.error("ERROR: Image Error: " + e);
			digilibError(errorMsgHtml, ERROR_IMAGE,
					"ERROR: Image Operation Error: " + e, response);
		} catch (RuntimeException e) {
			// JAI likes to throw RuntimeExceptions ;-(
			logger.error("ERROR: Other Image Error: " + e);
			digilibError(errorMsgHtml, ERROR_IMAGE,
					"ERROR: Other Image Operation Error: " + e, response);
		}
	}

	/**
	 * Sends an error to the client as text or image.
	 * 
	 * @param asHTML
	 * @param type
	 * @param msg
	 * @param response
	 */
	public void digilibError(boolean asHTML, int type, String msg,
			HttpServletResponse response) {
			try {
			File img = null;
			if (type == ERROR_AUTH) {
				if (msg == null) {
					msg = "ERROR: Unauthorized access!";
				}
				img = denyImgFile;
				} else {
				if (msg == null) {
					msg = "ERROR: Other image error!";
				}
				img = this.errorImgFile;
		}
			if (asHTML && (img != null)) {
				ServletOps.htmlMessage(msg, response);
			} else {
				ServletOps.sendFile(img, null, response);
	}
		} catch (IOException e) {
			logger.error("Error sending error!", e);
		}

	}

} //Scaler class