view servlet/src/digilib/servlet/Scaler.java @ 149:04ad64b2137a

Servlet version 1.14b1 - better performance with thumbnails (really, this time :-) - new DocuInfo class - new Directory class - DocuFile uses String and Directory as data members - parameter rearrangements
author robcast
date Tue, 26 Aug 2003 22:28:43 +0200
parents 837a633a0407
children bc8df0133c04
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 digilib.Utils;
import digilib.auth.AuthOpException;
import digilib.auth.AuthOps;
import digilib.image.DocuImage;
import digilib.image.DocuInfo;
import digilib.image.ImageLoaderImageInfoDocuInfo;
import digilib.image.ImageOpException;
import digilib.image.ImageSize;
import digilib.io.DocuDirCache;
import digilib.io.DocuFile;
import digilib.io.DocuFileset;
import digilib.io.FileOpException;
import digilib.io.FileOps;

//import tilecachetool.*;

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

	// digilib servlet version (for all components)
	public static final String dlVersion = "1.14b1";

	// Utils instance with debuglevel
	Utils util;
	// FileOps instance
	FileOps fileOp;
	// AuthOps instance
	AuthOps authOp;
	// ServletOps instance
	ServletOps servletOp;
	// DocuDirCache instance
	DocuDirCache dirCache;

	// DigilibConfiguration instance
	DigilibConfiguration dlConfig;

	// use authorization database
	boolean useAuthentication = true;

	// 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);

		// Debuggin!
		//TCTool tctool = new TCTool();
		System.out.println(
			"***** Digital Image Library 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) {
			// create new Configuration
			try {
				dlConfig = new DigilibConfiguration(config);
				context.setAttribute("digilib.servlet.configuration", dlConfig);
			} catch (Exception e) {
				throw new ServletException(e);
			}
		}
		// set the servlet version
		dlConfig.setServletVersion(dlVersion);
		// first we need an Utils
		util = dlConfig.getUtil();
		// set our AuthOps
		useAuthentication = dlConfig.isUseAuthentication();
		authOp = dlConfig.getAuthOp();
		// FileOps instance
		fileOp = new FileOps(util);
		// AuthOps instance
		servletOp = new ServletOps(util);
		// DocuDirCache instance
		dirCache = dlConfig.getDirCache();
	}

	/** Process the HTTP Get request*/
	public void doGet(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
		util.dprintln(1, "The servlet has received a GET!");
		// 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 {
		util.dprintln(1, "The servlet has received a POST!");
		// 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, IOException {

		// 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 = 1;
		// send html error message (or image file)
		boolean errorMsgHtml = false;
		// mirror the image
		boolean doMirror = false;
		// angle of mirror axis
		double mirrorAngle = 0;
		// original (hires) image resolution
		double origResX = 0;
		double origResY = 0;

		/*
		 *  request parameters
		 */

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

		// destination image width
		int paramDW = dlRequest.getDw();
		// destination image height
		int paramDH = dlRequest.getDh();
		// dw and dh shouldn't be empty, if they are, set dw=dh
		if (paramDW <= 0) {
			paramDW = paramDH;
		}
		if (paramDH <= 0) {
			paramDH = paramDW;
		}
		// relative area x_offset (0..1)
		double paramWX = dlRequest.getWx();
		// relative area y_offset
		double paramWY = dlRequest.getWy();
		// relative area width (0..1)
		double paramWW = dlRequest.getWw();
		// relative area height
		double paramWH = dlRequest.getWh();
		// scale factor (additional to dw/width, dh/height)
		double paramWS = dlRequest.getWs();
		// rotation angle
		double paramROT = dlRequest.getRot();
		// contrast enhancement
		float paramCONT = dlRequest.getCont();
		// brightness enhancement
		float paramBRGT = dlRequest.getBrgt();
		// color modification
		float[] paramRGBM = dlRequest.getRgbm();
		float[] paramRGBA = dlRequest.getRgba();
		// destination resolution (DPI)
		float paramDDPIX = dlRequest.getDdpix();
		float paramDDPIY = dlRequest.getDdpiy();
		if ((paramDDPIX == 0) || (paramDDPIY == 0)) {
			// if X or Y resolution isn't set, use DDPI
			paramDDPIX = dlRequest.getDdpi();
			paramDDPIY = dlRequest.getDdpi();
		}

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

		//"big" try for all file/image actions
		try {

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

			// new DocuInfo instance
			DocuInfo docuInfo = new ImageLoaderImageInfoDocuInfo();

			/*
			 *  find the file to load/send
			 */

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

			/*
			 * check permissions
			 */
			if (useAuthentication) {
				// get a list of required roles (empty if no restrictions)
				List rolesRequired = authOp.rolesForPath(loadPathName, request);
				if (rolesRequired != null) {
					util.dprintln(1, "Role required: " + rolesRequired);
					util.dprintln(2, "User: " + request.getRemoteUser());
					// is the current request/user authorized?
					if (!authOp.isRoleAuthorized(rolesRequired, request)) {
						// send deny answer and abort
						util.dprintln(1, "ERROR: access denied!");
						if (errorMsgHtml) {
							ServletOps.htmlMessage(
								"ERROR: Unauthorized access!",
								response);
						} else {
							servletOp.sendFile(
								new File(dlConfig.getDenyImgFileName()),
								response);
						}
						return;
					}
				}
			}

			// find the file(set)
			DocuFile fileToLoad;
			fileset = dirCache.getFileset(loadPathName, dlRequest.getPn());
			if (fileset == null) {
				throw new FileOpException(
					"File "
						+ loadPathName
						+ "("
						+ dlRequest.getPn()
						+ ") not found.");
			}

			/*
			 * calculate expected source image size
			 * 
			 */
			ImageSize expectedSourceSize = new ImageSize();
			if (scaleToFit) {
				double 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.get(0);
			} else if (loresOnly) {
				// enforced lores uses next smaller resolution
				fileToLoad =
					fileset.getNextSmaller(expectedSourceSize, docuInfo);
				if (fileToLoad == null) {
					// this is the smallest we have
					fileToLoad = fileset.get(fileset.size() - 1);
				}
			} else {
				// autores: use next higher resolution
				fileToLoad =
					fileset.getNextBigger(expectedSourceSize, docuInfo);
				if (fileToLoad == null) {
					// this is the highest we have
					fileToLoad = fileset.get(0);
				}
			}
			util.dprintln(1, "Loading: " + fileToLoad.getFile());

			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()) {
				docuInfo.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.isOption("hmir")
					|| dlRequest.isOption("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 ((loresOnly
				&& imageSendable
				&& fileToLoad.getSize().isSmallerThan(expectedSourceSize))
				|| (!(loresOnly || hiresOnly) && fileToLoad.getSize().fitsIn(expectedSourceSize))
				|| sendFile) {

				util.dprintln(1, "Sending File as is.");

				servletOp.sendFile(fileToLoad.getFile(), response);

				util.dprintln(
					1,
					"Done in "
						+ (System.currentTimeMillis() - startTime)
						+ "ms");
				return;
			}

			// new DocuImage instance
			DocuImage docuImage = dlConfig.getDocuImageInstance();
			if (docuImage == null) {
				throw new ImageOpException("Unable to load DocuImage class!");
			}

			// set interpolation quality
			docuImage.setQuality(scaleQual);

			/*
			 *  crop and scale the image
			 */

			util.dprintln(
				2,
				"IMG: " + imgSize.getWidth() + "x" + imgSize.getHeight());
			util.dprintln(
				2,
				"time " + (System.currentTimeMillis() - startTime) + "ms");

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

			// coordinates using Java2D
			// image size in pixels
			Rectangle2D imgBounds =
				new Rectangle2D.Double(
					0,
					0,
					imgSize.getWidth(),
					imgSize.getHeight());
			// user window area in [0,1] coordinates
			Rectangle2D relUserArea =
				new Rectangle2D.Double(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 = userImgArea.getWidth();
				areaHeight = 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
						e1.printStackTrace();
					}
				}
			}

			util.dprintln(
				1,
				"Scale "
					+ scaleXY
					+ "("
					+ scaleX
					+ ","
					+ scaleY
					+ ") on "
					+ outerUserImgArea);

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

			areaWidth = outerUserImgArea.getWidth();
			areaHeight = outerUserImgArea.getHeight();
			areaXoff = outerUserImgArea.getX();
			areaYoff = outerUserImgArea.getY();

			util.dprintln(
				2,
				"crop: "
					+ areaXoff
					+ ","
					+ areaYoff
					+ " "
					+ areaWidth
					+ "x"
					+ areaHeight);

			// check image parameters sanity
			if ((areaWidth < 1)
				|| (areaHeight < 1)
				|| (scaleXY * areaWidth < 2)
				|| (scaleXY * areaHeight < 2)) {
				util.dprintln(1, "ERROR: invalid scale parameter set!");
				throw new ImageOpException("Invalid scale parameter set!");
			}

			/* 
			 * crop and scale image
			 */

			// use subimage loading if possible
			if (docuImage.isSubimageSupported()) {
				System.out.println(
					"Subimage: scale " + scaleXY + " = " + (1 / scaleXY));
				double subf = 1d;
				double subsamp = 1d;
				if (scaleXY < 1) {
					subf = 1 / scaleXY;
					// for higher quality reduce subsample factor by minSubsample
					if (scaleQual > 0) {
						subsamp =
							Math.max(
								Math.floor(subf / dlConfig.getMinSubsample()),
								1d);
					} else {
						subsamp = Math.floor(subf);
					}
					scaleXY = subsamp / subf;
					System.out.println(
						"Using subsampling: " + subsamp + " rest " + scaleXY);
				}

				docuImage.loadSubimage(
					fileToLoad,
					outerUserImgArea.getBounds(),
					(int) subsamp);

				System.out.println(
					"SUBSAMP: "
						+ subsamp
						+ " -> "
						+ docuImage.getWidth()
						+ "x"
						+ docuImage.getHeight());

				docuImage.scale(scaleXY, scaleXY);

			} else {
				// else load and crop the whole file
				docuImage.loadImage(fileToLoad);
				docuImage.crop(
					(int) areaXoff,
					(int) areaYoff,
					(int) areaWidth,
					(int) areaHeight);

				docuImage.scale(scaleXY, scaleXY);
			}

			// mirror image
			// operation mode: "hmir": mirror horizontally, "vmir": mirror vertically
			if (dlRequest.isOption("hmir")) {
				docuImage.mirror(0);
			}
			if (dlRequest.isOption("vmir")) {
				docuImage.mirror(90);
			}

			// rotate image
			if (paramROT != 0) {
				docuImage.rotate(paramROT);
				if (wholeRotArea) {
					// crop to the inner bounding box
					double xcrop =
						docuImage.getWidth()
							- innerUserImgArea.getWidth() * scaleXY;
					double ycrop =
						docuImage.getHeight()
							- innerUserImgArea.getHeight() * scaleXY;
					if ((xcrop > 0) || (ycrop > 0)) {
						// only crop smaller
						xcrop = (xcrop > 0) ? xcrop : 0;
						ycrop = (ycrop > 0) ? ycrop : 0;
						// crop image
						docuImage.crop(
							(int) (xcrop / 2),
							(int) (ycrop / 2),
							(int) (docuImage.getWidth() - xcrop),
							(int) (docuImage.getHeight() - ycrop));
					}
				}

			}

			// color modification
			if ((paramRGBM != null) || (paramRGBA != null)) {
				// make shure we actually have two arrays 
				if (paramRGBM == null) {
					paramRGBM = new float[3];
				}
				if (paramRGBA == null) {
					paramRGBA = new float[3];
				}
				// calculate "contrast" values (c=2^x)
				float[] mult = new float[3];
				for (int i = 0; i < 3; i++) {
					mult[i] = (float) Math.pow(2, (double) paramRGBM[i]);
				}
				docuImage.enhanceRGB(mult, paramRGBA);
			}

			// contrast and brightness enhancement
			if ((paramCONT != 0) || (paramBRGT != 0)) {
				double mult = Math.pow(2, paramCONT);
				docuImage.enhance((float) mult, (float) paramBRGT);
			}

			util.dprintln(
				2,
				"time " + (System.currentTimeMillis() - startTime) + "ms");

			/*
			 *  write the resulting image
			 */

			// setup output -- if source is JPG then dest will be JPG else it's PNG
			if (mimeType.equals("image/jpeg")
				|| mimeType.equals("image/jp2")) {
				mimeType = "image/jpeg";
			} else {
				mimeType = "image/png";
			}
			response.setContentType(mimeType);

			// write the image
			docuImage.writeImage(mimeType, response.getOutputStream());

			util.dprintln(
				1,
				"Done in " + (System.currentTimeMillis() - startTime) + "ms");

			/*
			 *  error handling
			 */

		} // end of "big" try
		catch (FileOpException e) {
			util.dprintln(1, "ERROR: File IO Error: " + e);
			try {
				if (errorMsgHtml) {
					ServletOps.htmlMessage(
						"ERROR: File IO Error: " + e,
						response);
				} else {
					servletOp.sendFile(
						new File(dlConfig.getErrorImgFileName()),
						response);
				}
			} catch (FileOpException ex) {
			} // so we don't get a loop
		} catch (AuthOpException e) {
			util.dprintln(1, "ERROR: Authorization error: " + e);
			try {
				if (errorMsgHtml) {
					ServletOps.htmlMessage(
						"ERROR: Authorization error: " + e,
						response);
				} else {
					servletOp.sendFile(
						new File(dlConfig.getErrorImgFileName()),
						response);
				}
			} catch (FileOpException ex) {
			} // so we don't get a loop
		} catch (ImageOpException e) {
			util.dprintln(1, "ERROR: Image Error: " + e);
			try {
				if (errorMsgHtml) {
					ServletOps.htmlMessage(
						"ERROR: Image Operation Error: " + e,
						response);
				} else {
					servletOp.sendFile(
						new File(dlConfig.getErrorImgFileName()),
						response);
				}
			} catch (FileOpException ex) {
			} // so we don't get a loop
		} catch (RuntimeException e) {
			// JAI likes to throw RuntimeExceptions ;-(
			util.dprintln(1, "ERROR: Other Image Error: " + e);
			try {
				if (errorMsgHtml) {
					ServletOps.htmlMessage(
						"ERROR: Other Image Operation Error: " + e,
						response);
				} else {
					servletOp.sendFile(
						new File(dlConfig.getErrorImgFileName()),
						response);
				}
			} catch (FileOpException ex) {
			} // so we don't get a loop

		}
	}

} //Scaler class