All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.twelvemonkeys.servlet.image.ImageServletResponseImpl Maven / Gradle / Ivy

There is a newer version: 2.3
Show newest version
/*
 * Copyright (c) 2008, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * 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.
 *     * Neither the name "TwelveMonkeys" nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT OWNER 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.
 */

package com.twelvemonkeys.servlet.image;

import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.servlet.ServletResponseStreamDelegate;
import com.twelvemonkeys.servlet.ServletUtil;

import javax.imageio.*;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.servlet.ServletContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Iterator;

/**
 * This {@link ImageServletResponse} implementation can be used with image
 * requests, to have the image immediately decoded to a {@code BufferedImage}.
 * The image may be optionally subsampled, scaled and/or cropped.
 * The response also automtically handles writing the image back to the underlying response stream
 * in the preferred format, when the response is flushed.
 * 

* * @author Harald Kuhr * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java#10 $ * */ // TODO: Refactor out HTTP specifcs (if possible). // TODO: Is it a good ide to throw IIOException? class ImageServletResponseImpl extends HttpServletResponseWrapper implements ImageServletResponse { private final ServletRequest mOriginalRequest; private final ServletContext mContext; private final ServletResponseStreamDelegate mStreamDelegate; private FastByteArrayOutputStream mBufferedOut; private RenderedImage mImage; private String mOutputContentType; private String mOriginalContentType; private int mOriginalContentLength = -1; /** * Creates an {@code ImageServletResponseImpl}. * * @param pRequest the request * @param pResponse the response * @param pContext the servlet context */ public ImageServletResponseImpl(final HttpServletRequest pRequest, final HttpServletResponse pResponse, final ServletContext pContext) { super(pResponse); mOriginalRequest = pRequest; mStreamDelegate = new ServletResponseStreamDelegate(pResponse) { @Override protected OutputStream createOutputStream() throws IOException { if (mOriginalContentLength >= 0) { mBufferedOut = new FastByteArrayOutputStream(mOriginalContentLength); } else { mBufferedOut = new FastByteArrayOutputStream(0); } return mBufferedOut; } }; mContext = pContext; } /** * Creates an {@code ImageServletResponseImpl}. * * @param pRequest the request * @param pResponse the response * @param pContext the servlet context * * @throws ClassCastException if {@code pRequest} is not an {@link javax.servlet.http.HttpServletRequest} or * {@code pResponse} is not an {@link javax.servlet.http.HttpServletResponse}. */ public ImageServletResponseImpl(final ServletRequest pRequest, final ServletResponse pResponse, final ServletContext pContext) { // Cheat for now... this((HttpServletRequest) pRequest, (HttpServletResponse) pResponse, pContext); } /** * Called by the container, do not invoke. * * @param pMimeType the content (MIME) type */ public void setContentType(final String pMimeType) { // Throw exception is allready set if (mOriginalContentType != null) { throw new IllegalStateException("ContentType allready set."); } mOriginalContentType = pMimeType; } /** * Called by the container. Do not invoke. * * @return the response's {@code OutputStream} * @throws IOException */ public ServletOutputStream getOutputStream() throws IOException { return mStreamDelegate.getOutputStream(); } /** * Called by the container. Do not invoke. * * @return the response's {@code PrintWriter} * @throws IOException */ public PrintWriter getWriter() throws IOException { return mStreamDelegate.getWriter(); } /** * Called by the container. Do not invoke. * * @param pLength the content length */ public void setContentLength(final int pLength) { if (mOriginalContentLength != -1) { throw new IllegalStateException("ContentLength already set."); } mOriginalContentLength = pLength; } /** * Writes the image to the original {@code ServletOutputStream}. * If no format is set in this response, the image is encoded in the same * format as the original image. * * @throws IOException if an I/O exception occurs during writing */ public void flush() throws IOException { String outputType = getOutputContentType(); // Force transcoding, if no other filtering is done if (!outputType.equals(mOriginalContentType)) { getImage(); } // This is stupid, but don't know how to work around... // TODO: Test what types of images that work with JPEG, consider reporting it as a bug if (("image/jpeg".equals(outputType) || "image/jpg".equals(outputType) || "image/bmp".equals(outputType) || "image/x-bmp".equals(outputType)) && mImage instanceof BufferedImage && ((BufferedImage) mImage).getType() == BufferedImage.TYPE_INT_ARGB) { mImage = ImageUtil.toBuffered(mImage, BufferedImage.TYPE_INT_RGB); } //System.out.println("Writing image, content-type: " + getContentType(outputType)); //System.out.println("Writing image, outputType: " + outputType); //System.out.println("Writing image: " + mImage); if (mImage != null) { Iterator writers = ImageIO.getImageWritersByMIMEType(outputType); if (writers.hasNext()) { super.setContentType(outputType); OutputStream out = super.getOutputStream(); ImageWriter writer = (ImageWriter) writers.next(); try { ImageWriteParam param = writer.getDefaultWriteParam(); Float requestQuality = (Float) mOriginalRequest.getAttribute(ImageServletResponse.ATTRIB_OUTPUT_QUALITY); // The default JPEG quality is not good enough, so always apply compression if ((requestQuality != null || "jpeg".equalsIgnoreCase(getFormatNameSafe(writer))) && param.canWriteCompressed()) { param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); param.setCompressionQuality(requestQuality != null ? requestQuality : 0.8f); } ImageOutputStream stream = ImageIO.createImageOutputStream(out); //System.out.println("-ISR- Image: " + mImage); //System.out.println("-ISR- ImageWriter: " + writer); //System.out.println("-ISR- ImageOutputStream: " + stream); writer.setOutput(stream); try { writer.write(null, new IIOImage(mImage, null, null), param); } finally { stream.close(); } } finally { writer.dispose(); out.flush(); // out.close(); } } else { mContext.log("ERROR: No writer for content-type: " + outputType); // sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unable to encode image: No writer for content-type " + outputType); throw new IIOException("Unable to transcode image: No suitable image writer found (content-type: " + outputType + ")."); } } else { super.setContentType(mOriginalContentType); ServletOutputStream out = super.getOutputStream(); try { mBufferedOut.writeTo(out); } finally { out.flush(); } } } private String getFormatNameSafe(final ImageWriter pWriter) { try { return pWriter.getOriginatingProvider().getFormatNames()[0]; } catch (RuntimeException e) { // NPE, AIOOBE, etc.. return null; } } public String getOutputContentType() { return mOutputContentType != null ? mOutputContentType : mOriginalContentType; } public void setOutputContentType(final String pImageFormat) { mOutputContentType = pImageFormat; } /** * Sets the image for this response. * * @param pImage the {@code RenderedImage} that will be written to the * response stream */ public void setImage(final RenderedImage pImage) { mImage = pImage; } /** * Gets the decoded image from the response. * * @return a {@code BufferedImage} or {@code null} if the image could * not be read. * * @throws java.io.IOException if an I/O exception occurs during reading */ public BufferedImage getImage() throws IOException { if (mImage == null) { // No content, no image if (mBufferedOut == null) { return null; } // Read from the byte buffer InputStream byteStream = mBufferedOut.createInputStream(); ImageInputStream input = null; try { input = ImageIO.createImageInputStream(byteStream); Iterator readers = ImageIO.getImageReaders(input); if (readers.hasNext()) { // Get the correct reader ImageReader reader = (ImageReader) readers.next(); try { reader.setInput(input); ImageReadParam param = reader.getDefaultReadParam(); // Get default size int originalWidth = reader.getWidth(0); int originalHeight = reader.getHeight(0); // Extract AOI from request Rectangle aoi = extractAOIFromRequest(originalWidth, originalHeight); if (aoi != null) { param.setSourceRegion(aoi); originalWidth = aoi.width; originalHeight = aoi.height; } // If possible, extract size from request Dimension size = extractSizeFromRequest(originalWidth, originalHeight); double readSubSamplingFactor = getReadSubsampleFactorFromRequest(); if (size != null) { //System.out.println("Size: " + size); if (param.canSetSourceRenderSize()) { param.setSourceRenderSize(size); } else { int subX = (int) Math.max(originalWidth / (double) (size.width * readSubSamplingFactor), 1.0); int subY = (int) Math.max(originalHeight / (double) (size.height * readSubSamplingFactor), 1.0); if (subX > 1 || subY > 1) { param.setSourceSubsampling(subX, subY, subX > 1 ? subX / 2 : 0, subY > 1 ? subY / 2 : 0); } } } // Need base URI for SVG with links/stylesheets etc maybeSetBaseURIFromRequest(param); // Finally, read the image using the supplied parameter BufferedImage image = reader.read(0, param); // If reader doesn't support dynamic sizing, scale now if (image != null && size != null && (image.getWidth() != size.width || image.getHeight() != size.height)) { int resampleAlgorithm = getResampleAlgorithmFromRequest(); // NOTE: Only use createScaled if IndexColorModel, // as it's more expensive due to color conversion if (image.getColorModel() instanceof IndexColorModel) { image = ImageUtil.createScaled(image, size.width, size.height, resampleAlgorithm); } else { image = ImageUtil.createResampled(image, size.width, size.height, resampleAlgorithm); } } // Fill bgcolor behind image, if transparent extractAndSetBackgroundColor(image); //System.out.println("-ISR- Image: " + image); // Set image mImage = image; } finally { reader.dispose(); } } else { mContext.log("ERROR: No suitable image reader found (content-type: " + mOriginalContentType + ")."); mContext.log("ERROR: Available formats: " + getFormatsString()); throw new IIOException("Unable to transcode image: No suitable image reader found (content-type: " + mOriginalContentType + ")."); } // Free resources, as the image is now either read, or unreadable mBufferedOut = null; } finally { if (input != null) { input.close(); } } } // Image is usually a BufferedImage, but may also be a RenderedImage return mImage != null ? ImageUtil.toBuffered(mImage) : null; } private int getResampleAlgorithmFromRequest() { int resampleAlgoithm; Object algorithm = mOriginalRequest.getAttribute(ATTRIB_IMAGE_RESAMPLE_ALGORITHM); if (algorithm instanceof Integer && ((Integer) algorithm == Image.SCALE_SMOOTH || (Integer) algorithm == Image.SCALE_FAST || (Integer) algorithm == Image.SCALE_DEFAULT)) { resampleAlgoithm = (Integer) algorithm; } else { if (algorithm != null) { mContext.log("WARN: Illegal image resampling algorithm: " + algorithm); } resampleAlgoithm = BufferedImage.SCALE_DEFAULT; } return resampleAlgoithm; } private double getReadSubsampleFactorFromRequest() { double subsampleFactor; Object factor = mOriginalRequest.getAttribute(ATTRIB_READ_SUBSAMPLING_FACTOR); if (factor instanceof Number && ((Number) factor).doubleValue() >= 1.0) { subsampleFactor = ((Number) factor).doubleValue(); } else { if (factor != null) { mContext.log("WARN: Illegal read subsampling factor: " + factor); } subsampleFactor = 2.0; } return subsampleFactor; } private void extractAndSetBackgroundColor(final BufferedImage pImage) { // TODO: bgColor request attribute instead of parameter? if (pImage.getColorModel().hasAlpha()) { String bgColor = mOriginalRequest.getParameter("bg.color"); if (bgColor != null) { Color color = StringUtil.toColor(bgColor); Graphics2D g = pImage.createGraphics(); try { g.setColor(color); g.setComposite(AlphaComposite.DstOver); g.fillRect(0, 0, pImage.getWidth(), pImage.getHeight()); } finally { g.dispose(); } } } } private static String getFormatsString() { String[] formats = ImageIO.getReaderFormatNames(); StringBuilder buf = new StringBuilder(); for (int i = 0; i < formats.length; i++) { String format = formats[i]; if (i > 0) { buf.append(", "); } buf.append(format); } return buf.toString(); } private void maybeSetBaseURIFromRequest(final ImageReadParam pParam) { if (mOriginalRequest instanceof HttpServletRequest) { try { // If there's a setBaseURI method, we'll try to use that (uses reflection, to avoid dependency on plugins) Method setBaseURI; try { setBaseURI = pParam.getClass().getMethod("setBaseURI", String.class); } catch (NoSuchMethodException ignore) { return; } // Get URL for resource and set as base String baseURI = ServletUtil.getContextRelativeURI((HttpServletRequest) mOriginalRequest); URL resourceURL = mContext.getResource(baseURI); if (resourceURL == null) { resourceURL = ServletUtil.getRealURL(mContext, baseURI); } if (resourceURL != null) { setBaseURI.invoke(pParam, resourceURL.toExternalForm()); } else { mContext.log("WARN: Resource URL not found for URI: " + baseURI); } } catch (Exception e) { mContext.log("WARN: Could not set base URI: ", e); } } } private Dimension extractSizeFromRequest(final int pDefaultWidth, final int pDefaultHeight) { // TODO: Allow extraction from request parameters /* int sizeW = ServletUtil.getIntParameter(mOriginalRequest, "size.w", -1); int sizeH = ServletUtil.getIntParameter(mOriginalRequest, "size.h", -1); boolean sizePercent = ServletUtil.getBooleanParameter(mOriginalRequest, "size.percent", false); boolean sizeUniform = ServletUtil.getBooleanParameter(mOriginalRequest, "size.uniform", true); */ Dimension size = (Dimension) mOriginalRequest.getAttribute(ATTRIB_SIZE); int sizeW = size != null ? size.width : -1; int sizeH = size != null ? size.height : -1; Boolean b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_SIZE_PERCENT); boolean sizePercent = b != null && b; // default: false b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_SIZE_UNIFORM); boolean sizeUniform = b == null || b; // default: true if (sizeW >= 0 || sizeH >= 0) { size = getSize(pDefaultWidth, pDefaultHeight, sizeW, sizeH, sizePercent, sizeUniform); } return size; } private Rectangle extractAOIFromRequest(final int pDefaultWidth, final int pDefaultHeight) { // TODO: Allow extraction from request parameters /* int aoiX = ServletUtil.getIntParameter(mOriginalRequest, "aoi.x", -1); int aoiY = ServletUtil.getIntParameter(mOriginalRequest, "aoi.y", -1); int aoiW = ServletUtil.getIntParameter(mOriginalRequest, "aoi.w", -1); int aoiH = ServletUtil.getIntParameter(mOriginalRequest, "aoi.h", -1); boolean aoiPercent = ServletUtil.getBooleanParameter(mOriginalRequest, "aoi.percent", false); boolean aoiUniform = ServletUtil.getBooleanParameter(mOriginalRequest, "aoi.uniform", false); */ Rectangle aoi = (Rectangle) mOriginalRequest.getAttribute(ATTRIB_AOI); int aoiX = aoi != null ? aoi.x : -1; int aoiY = aoi != null ? aoi.y : -1; int aoiW = aoi != null ? aoi.width : -1; int aoiH = aoi != null ? aoi.height : -1; Boolean b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_AOI_PERCENT); boolean aoiPercent = b != null && b; // default: false b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_AOI_UNIFORM); boolean aoiUniform = b != null && b; // default: false if (aoiX >= 0 || aoiY >= 0 || aoiW >= 0 || aoiH >= 0) { aoi = getAOI(pDefaultWidth, pDefaultHeight, aoiX, aoiY, aoiW, aoiH, aoiPercent, aoiUniform); return aoi; } return null; } // TODO: Move these to ImageUtil or similar, as they are often used... // TODO: Consider separate methods for percent and pixels /** * Gets the dimensions (height and width) of the scaled image. The * dimensions are computed based on the old image's dimensions, the units * used for specifying new dimensions and whether or not uniform scaling * should be used (se algorithm below). * * @param pOriginalWidth the original width of the image * @param pOriginalHeight the original height of the image * @param pWidth the new width of the image, or -1 if unknown * @param pHeight the new height of the image, or -1 if unknown * @param pPercent the constant specifying units for width and height * parameter (UNITS_PIXELS or UNITS_PERCENT) * @param pUniformScale boolean specifying uniform scale or not * @return a Dimension object, with the correct width and heigth * in pixels, for the scaled version of the image. */ protected static Dimension getSize(int pOriginalWidth, int pOriginalHeight, int pWidth, int pHeight, boolean pPercent, boolean pUniformScale) { // If uniform, make sure width and height are scaled the same ammount // (use ONLY height or ONLY width). // // Algoritm: // if uniform // if newHeight not set // find ratio newWidth / oldWidth // oldHeight *= ratio // else if newWidth not set // find ratio newWidth / oldWidth // oldHeight *= ratio // else // find both ratios and use the smallest one // (this will be the largest version of the image that fits // inside the rectangle given) // (if PERCENT, just use smallest percentage). // // If units is percent, we only need old height and width float ratio; if (pPercent) { if (pWidth >= 0 && pHeight >= 0) { // Non-uniform pWidth = Math.round((float) pOriginalWidth * (float) pWidth / 100f); pHeight = Math.round((float) pOriginalHeight * (float) pHeight / 100f); } else if (pWidth >= 0) { // Find ratio from pWidth ratio = (float) pWidth / 100f; pWidth = Math.round((float) pOriginalWidth * ratio); pHeight = Math.round((float) pOriginalHeight * ratio); } else if (pHeight >= 0) { // Find ratio from pHeight ratio = (float) pHeight / 100f; pWidth = Math.round((float) pOriginalWidth * ratio); pHeight = Math.round((float) pOriginalHeight * ratio); } // Else: No scale } else { if (pUniformScale) { if (pWidth >= 0 && pHeight >= 0) { // Compute both ratios ratio = (float) pWidth / (float) pOriginalWidth; float heightRatio = (float) pHeight / (float) pOriginalHeight; // Find the largest ratio, and use that for both if (heightRatio < ratio) { ratio = heightRatio; pWidth = Math.round((float) pOriginalWidth * ratio); } else { pHeight = Math.round((float) pOriginalHeight * ratio); } } else if (pWidth >= 0) { // Find ratio from pWidth ratio = (float) pWidth / (float) pOriginalWidth; pHeight = Math.round((float) pOriginalHeight * ratio); } else if (pHeight >= 0) { // Find ratio from pHeight ratio = (float) pHeight / (float) pOriginalHeight; pWidth = Math.round((float) pOriginalWidth * ratio); } // Else: No scale } } // Default is no scale, just work as a proxy if (pWidth < 0) { pWidth = pOriginalWidth; } if (pHeight < 0) { pHeight = pOriginalHeight; } // Create new Dimension object and return return new Dimension(pWidth, pHeight); } protected static Rectangle getAOI(int pOriginalWidth, int pOriginalHeight, int pX, int pY, int pWidth, int pHeight, boolean pPercent, boolean pUniform) { // Algoritm: // Try to get x and y (default 0,0). // Try to get width and height (default width-x, height-y) // // If percent, get ratio // // If uniform // float ratio; if (pPercent) { if (pWidth >= 0 && pHeight >= 0) { // Non-uniform pWidth = Math.round((float) pOriginalWidth * (float) pWidth / 100f); pHeight = Math.round((float) pOriginalHeight * (float) pHeight / 100f); } else if (pWidth >= 0) { // Find ratio from pWidth ratio = (float) pWidth / 100f; pWidth = Math.round((float) pOriginalWidth * ratio); pHeight = Math.round((float) pOriginalHeight * ratio); } else if (pHeight >= 0) { // Find ratio from pHeight ratio = (float) pHeight / 100f; pWidth = Math.round((float) pOriginalWidth * ratio); pHeight = Math.round((float) pOriginalHeight * ratio); } // Else: No crop } else { // Uniform if (pUniform) { if (pWidth >= 0 && pHeight >= 0) { // Compute both ratios ratio = (float) pWidth / (float) pHeight; float originalRatio = (float) pOriginalWidth / (float) pOriginalHeight; if (ratio > originalRatio) { pWidth = pOriginalWidth; pHeight = Math.round((float) pOriginalWidth / ratio); } else { pHeight = pOriginalHeight; pWidth = Math.round((float) pOriginalHeight * ratio); } } else if (pWidth >= 0) { // Find ratio from pWidth ratio = (float) pWidth / (float) pOriginalWidth; pHeight = Math.round((float) pOriginalHeight * ratio); } else if (pHeight >= 0) { // Find ratio from pHeight ratio = (float) pHeight / (float) pOriginalHeight; pWidth = Math.round((float) pOriginalWidth * ratio); } // Else: No crop } } // Not specified, or outside bounds: Use original dimensions if (pWidth < 0 || (pX < 0 && pWidth > pOriginalWidth) || (pX >= 0 && (pX + pWidth) > pOriginalWidth)) { pWidth = (pX >= 0 ? pOriginalWidth - pX : pOriginalWidth); } if (pHeight < 0 || (pY < 0 && pHeight > pOriginalHeight) || (pY >= 0 && (pY + pHeight) > pOriginalHeight)) { pHeight = (pY >= 0 ? pOriginalHeight - pY : pOriginalHeight); } // Center if (pX < 0) { pX = (pOriginalWidth - pWidth) / 2; } if (pY < 0) { pY = (pOriginalHeight - pHeight) / 2; } // System.out.println("x: " + pX + " y: " + pY // + " w: " + pWidth + " h " + pHeight); return new Rectangle(pX, pY, pWidth, pHeight); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy