/* The contents of this file are subject to the license and copyright terms * detailed in the license directory at the root of the source tree (also * available online at http://fedora-commons.org/license/). */ /** * Image manipulations (with the exception of the watermarking function) * are handled by ImageJ, a Java API written for image processing. * * Image encoding and decoding is handled by JAI, the Java Advanced Imaging * API, with the exception of GIF encoding, which is handled by ImageJ. * * ImageJ Information: * * Rasband, W.S., ImageJ, National Institutes of Health, Bethesda, * Maryland, USA, http://rsb.info.nih.gov/ij/, 1997-2003. * * The GifEncoder portion of ImageJ is copyrighted below: * * Transparency handling and variable bit size courtesy of Jack Palevich. * * Copyright (C) 1996 by Jef Poskanzer <jef@acme.com>. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * Visit the ACME Labs Java page for up-to-date versions of this and other * fine Java utilities: http://www.acme.com/java/ * */ package org.fcrepo.localservices.imagemanip; import ij.ImagePlus; import ij.io.FileInfo; import ij.io.GifEncoder; import ij.process.ImageProcessor; import ij.process.MedianCut; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.JAI; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.params.ClientPNames; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.apache.http.params.CoreConnectionPNames; import com.sun.media.jai.codec.BMPEncodeParam; import com.sun.media.jai.codec.ImageCodec; import com.sun.media.jai.codec.ImageEncodeParam; import com.sun.media.jai.codec.JPEGEncodeParam; import com.sun.media.jai.codec.MemoryCacheSeekableStream; import com.sun.media.jai.codec.PNGEncodeParam; import com.sun.media.jai.codec.TIFFEncodeParam; /** * ImageManipulation is a Java servlet that takes a URL of an image as a param * and based on other given parameters, can perform a variety of image related * manipulations on the object. * * <p>After the image is manipulated, it is then sent back as an image/type * object to the calling parent, most often a browser or an HTML img tag. * * @author Theodore Serbinski */ public class ImageManipulation extends HttpServlet { private static final long serialVersionUID = 1L; private static final Logger LOGGER = Logger.getLogger( "org.apache.catalina.core.ContainerBase.[Catalina].[localhost]"); private String inputMimeType; private boolean alreadyConvertedToRGB = false; private final PoolingClientConnectionManager cManager = getConnectionManager(); private PoolingClientConnectionManager getConnectionManager() { PoolingClientConnectionManager cm = new PoolingClientConnectionManager(); cm.getSchemeRegistry().register( new Scheme("https", 443, SSLSocketFactory.getSocketFactory())); cm.getSchemeRegistry().register( new Scheme("https-tomcat", 8443, SSLSocketFactory.getSocketFactory())); cm.getSchemeRegistry().register( new Scheme("http", 80, PlainSocketFactory.getSocketFactory())); cm.getSchemeRegistry().register( new Scheme("http-tomcat", 8080, PlainSocketFactory.getSocketFactory())); return cm; } @Override public void destroy() { cManager.shutdown(); } /** * Method automatically called by browser to handle image manipulations. * * @param req * Browser request to servlet res Response sent back to browser after * image manipulation * @throws IOException * If an input or output exception occurred ServletException If a * servlet exception occurred */ @Override public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { System.setProperty("java.awt.headless", "true"); // collect all possible parameters for servlet String url = req.getParameter("url"); String op = req.getParameter("op"); String newWidth = req.getParameter("newWidth"); String brightAmt = req.getParameter("brightAmt"); String zoomAmt = req.getParameter("zoomAmt"); String wmText = req.getParameter("wmText"); String cropX = req.getParameter("cropX"); String cropY = req.getParameter("cropY"); String cropWidth = req.getParameter("cropWidth"); String cropHeight = req.getParameter("cropHeight"); String convertTo = req.getParameter("convertTo"); if (convertTo != null) { convertTo = convertTo.toLowerCase(); } try { if (op == null) { throw new ServletException("op parameter not specified."); } String outputMimeType; // get the image via url and put it into the ImagePlus processor. BufferedImage img = getImage(url); // do watermarking stuff if (op.equals("watermark")) { if (wmText == null) { throw new ServletException("Must specify wmText."); } Graphics g = img.getGraphics(); int fontSize = img.getWidth() * 3 / 100; if (fontSize < 10) { fontSize = 10; } g.setFont(new Font("Lucida Sans", Font.BOLD, fontSize)); FontMetrics fm = g.getFontMetrics(); int stringWidth = (int) fm.getStringBounds(wmText, g).getWidth(); int x = img.getWidth() / 2 - stringWidth / 2; int y = img.getHeight() - fm.getHeight(); g.setColor(new Color(180, 180, 180)); g.fill3DRect(x - 10, y - fm.getHeight() - 4, stringWidth + 20, fm.getHeight() + 12, true); g.setColor(new Color(100, 100, 100)); g.drawString(wmText, x + 2, y + 2); g.setColor(new Color(240, 240, 240)); g.drawString(wmText, x, y); } ImageProcessor ip = new ImagePlus("temp", img).getProcessor(); // if the inputMimeType is image/gif, need to convert to RGB in any case if (inputMimeType.equals("image/gif")) { ip = ip.convertToRGB(); alreadyConvertedToRGB = true; } // causes scale() and resize() to do bilinear interpolation ip.setInterpolate(true); if (!op.equals("convert")) { if (op.equals("resize")) { ip = resize(ip, newWidth); } else if (op.equals("zoom")) { ip = zoom(ip, zoomAmt); } else if (op.equals("brightness")) { ip = brightness(ip, brightAmt); } else if (op.equals("watermark")) { // this is now taken care of beforehand (see above) } else if (op.equals("grayscale")) { ip = grayscale(ip); } else if (op.equals("crop")) { ip = crop(ip, cropX, cropY, cropWidth, cropHeight); } else { throw new ServletException("Invalid operation: " + op); } outputMimeType = inputMimeType; } else { if (convertTo == null) { throw new ServletException("Neither op nor convertTo was specified."); } if (convertTo.equals("jpg") || convertTo.equals("jpeg")) { outputMimeType = "image/jpeg"; } else if (convertTo.equals("gif")) { outputMimeType = "image/gif"; } else if (convertTo.equals("tiff")) { outputMimeType = "image/tiff"; } else if (convertTo.equals("bmp")) { outputMimeType = "image/bmp"; } else if (convertTo.equals("png")) { outputMimeType = "image/png"; } else { throw new ServletException("Invalid format: " + convertTo); } } res.setContentType(outputMimeType); BufferedOutputStream out = new BufferedOutputStream(res.getOutputStream()); outputImage(ip, out, outputMimeType); out.flush(); out.close(); } catch (Exception e) { e.printStackTrace(); res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e .getClass().getName() + ": " + e.getMessage()); } } /** * Gets and deserializes the image at the given URL into an Image object. * This method also sets the inputMimeType based on the HTTP Content-Type * header so that, if the image needs to be returned in it's original * format, the correct mime type can be sent in the response header. If the * input image is not a gif, jpg, tiff, bmp, or png (according to the http * response header), or some other kind of error occurs while reading the * stream from the remote host, a ServletException is thrown. * * @param url * The location of the input image. * @return Image The image object, if successful. * @throws Exception * If any of the aforementioned problems occurs. */ private BufferedImage getImage(String url) throws Exception { HttpGet get = null; LOGGER.info("ImageManipulation: GET " + url); try { DefaultHttpClient client = new DefaultHttpClient(cManager); client.getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 20000); get = new HttpGet(url); client.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, true); HttpResponse response = client.execute(get); int resultCode = response.getStatusLine().getStatusCode(); if (resultCode != 200) { throw new ServletException("Could not load image: " + url + ". Errorcode " + resultCode + " from remote server."); } LOGGER.info("ImageManipulation: STATUS " + resultCode); inputMimeType = response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue(); LOGGER.info("ImageManipulation: Content-Type " + inputMimeType); if (inputMimeType.equals("image/gif") || inputMimeType.equals("image/jpeg") || inputMimeType.equals("image/tiff") || inputMimeType.equals("image/bmp") || inputMimeType.equals("image/x-ms-bmp") || inputMimeType.equals("image/x-bitmap") || inputMimeType.equals("image/png")) { if (inputMimeType.endsWith("p")) { inputMimeType = "image/bmp"; // windows bitmaps are most } // commonly supported with this // mime type, even though it's not // an IANA-registered image type return JAI.create("stream", new MemoryCacheSeekableStream(response .getEntity().getContent())) .getAsBufferedImage(); } else { throw new ServletException("Source image was not a gif, png, " + "bmp, tiff, or jpg."); } } catch (Exception e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); throw e; } finally { if (get != null) { get.releaseConnection(); } } } private void outputImage(ImageProcessor ip, OutputStream out, String outputMimeType) throws Exception { if (outputMimeType.equals("image/gif")) { if (!alreadyConvertedToRGB) { ip = ip.convertToRGB(); } MedianCut mc = new MedianCut((int[]) ip.getPixels(), ip.getWidth(), ip .getHeight()); ip = mc.convertToByte(256); ImagePlus imp = new ImagePlus("temp", ip); FileInfo fi = imp.getFileInfo(); byte pixels[] = (byte[]) imp.getProcessor().getPixels(); GifEncoder ge = new GifEncoder(fi.width, fi.height, pixels, fi.reds, fi.greens, fi.blues); ge.write(out); } else { ImageEncodeParam param = null; String format = null; if (outputMimeType.equals("image/jpeg")) { param = new JPEGEncodeParam(); format = "JPEG"; } else if (outputMimeType.equals("image/tiff")) { param = new TIFFEncodeParam(); format = "TIFF"; } else if (outputMimeType.equals("image/bmp")) { param = new BMPEncodeParam(); format = "BMP"; } else if (outputMimeType.equals("image/png")) { param = new PNGEncodeParam.RGB(); format = "PNG"; } ImageCodec.createImageEncoder(format, out, param).encode(JAI .create("AWTImage", ip.createImage())); } } /** * Resizes an image to the supplied new width in pixels. The height is * reduced proportionally to the new width. * * @param ip * The image to resize newWidth The width in pixels to resize the * image to * @return The image resized */ private ImageProcessor resize(ImageProcessor ip, String newWidth) { if (newWidth != null) { try { int width = Integer.parseInt(newWidth); if (width < 0) { return ip; } int imgWidth = ip.getWidth(); int imgHeight = ip.getHeight(); ip = ip.resize(width, width * imgHeight / imgWidth); } // no need to do anything with number format exception since the servlet // returns only images; just return the original image catch (NumberFormatException e) { } } return ip; } /** * Zooms either in or out of an image by a supplied amount. The zooming * occurs from the center of the image. * * @param ip * The image to zoom zoomAmt The amount to zoom the image. 0 < * zoomAmt < 1 : zoom out 1 = zoomAmt : original image 1 < zoomAmt : * zoom in * @return The image zoomed */ private ImageProcessor zoom(ImageProcessor ip, String zoomAmt) { if (zoomAmt != null) { try { float zoom = Float.parseFloat(zoomAmt); if (zoom < 0) { return ip; } ip.scale(zoom, zoom); // if the image is being zoomed out, trim the extra whitespace around the image if (zoom < 1) { int imgWidth = ip.getWidth(); int imgHeight = ip.getHeight(); // set a ROI around the image, minus the extra whitespace ip.setRoi(Math.round(imgWidth / 2 - imgWidth * zoom / 2), Math.round(imgHeight / 2 - imgHeight * zoom / 2), Math.round(imgWidth * zoom), Math.round(imgHeight * zoom)); ip = ip.crop(); } } // no need to do anything with number format exception since the servlet // returns only images; just return the original image catch (NumberFormatException e) { } } return ip; } /** * Adjusts the brightness of an image by a supplied amount. * * @param ip * The image to adjust the brightness of brightAmt The amount to * adjust the brightness of the image by 0 <= brightAmt < 1 : darkens * image 1 = brightAmt : original image 1 < brightAmt : brightens * image * @return The image with brightness levels adjusted */ private ImageProcessor brightness(ImageProcessor ip, String brightAmt) { if (brightAmt != null) { try { float bright = Float.parseFloat(brightAmt); if (bright < 0) { return ip; } ip.multiply(bright); } // no need to do anything with number format exception since the servlet // returns only images; just return the original image catch (NumberFormatException e) { } } return ip; } /** * Converts an image to gray scale. * * @param ip * The image to convert to grayscale * @return The image converted to grayscale */ private ImageProcessor grayscale(ImageProcessor ip) { ip = ip.convertToByte(true); return ip; } /** * Crops an image with supplied starting point and ending point. * * @param ip * The image to crop cropX The starting x position; x=0 corresponds * to left side of image cropY The starting y position; y=0 * corresponds to top of image cropWidth The width of the crop, * starting from the above x cropHeight The height of the crop, * starting from the above y * @return The image cropped */ public ImageProcessor crop(ImageProcessor ip, String cropX, String cropY, String cropWidth, String cropHeight) { if (cropX != null && cropY != null) { try { int x = Integer.parseInt(cropX); int y = Integer.parseInt(cropY); int width; int height; // if value for cropWidth is not given, just use the width of the image if (cropWidth != null) { width = Integer.parseInt(cropWidth); } else { width = ip.getWidth(); } // if value for cropHeight is not given, just use the height of the image if (cropHeight != null) { height = Integer.parseInt(cropHeight); } else { height = ip.getHeight(); } // if any value is negative, this causes ImageJ to explode, so just return if (x < 0 || y < 0 || width < 0 || height < 0) { return ip; } ip.setRoi(x, y, width, height); ip = ip.crop(); } // no need to do anything with number format exception since the servlet // returns only images; just return the original image catch (NumberFormatException e) { } } return ip; } }