org.apache.cocoon.reading.ImageReader.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.cocoon.reading.ImageReader.java

Source

/*
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.cocoon.reading;

import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.RescaleOp;
import java.awt.image.WritableRaster;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Map;
import javax.swing.ImageIcon;

import org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.commons.lang.SystemUtils;
import org.xml.sax.SAXException;

import com.sun.image.codec.jpeg.ImageFormatException;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGEncodeParam;
import com.sun.image.codec.jpeg.JPEGImageEncoder;

/**
 * The <code>ImageReader</code> component is used to serve binary image data
 * in a sitemap pipeline. It makes use of HTTP Headers to determine if
 * the requested resource should be written to the <code>OutputStream</code>
 * or if it can signal that it hasn't changed.
 *
 * Parameters:
 *   <dl>
 *     <dt>&lt;width&gt;</dt>
 *     <dd> This parameter is optional. When specified, it determines the
 *          width of the binary image.
 *          If no height parameter is specified, the aspect ratio
 *          of the image is kept. The parameter may be expressed as an int or a percentage.
 *     </dd>
 *     <dt>&lt;height&gt;</dt>
 *     <dd> This parameter is optional. When specified, it determines the
 *          height of the binary image.
 *          If no width parameter is specified, the aspect ratio
 *          of the image is kept. The parameter may be expressed as an int or a percentage.
 *     </dd>
 *     <dt>&lt;scale(Red|Green|Blue)&gt;</dt>
 *     <dd>This parameter is optional. When specified it will cause the
 *         specified color component in the image to be multiplied by the
 *         specified floating point value.
 *     </dd>
 *     <dt>&lt;offset(Red|Green|Blue)&gt;</dt>
 *     <dd>This parameter is optional. When specified it will cause the
 *         specified color component in the image to be incremented by the
 *         specified floating point value.
 *     </dd>
 *     <dt>&lt;grayscale&gt;</dt>
 *     <dd>This parameter is optional. When specified and set to true it
 *         will cause each image pixel to be normalized. Default is "false".
 *     </dd>
 *     <dt>&lt;allow-enlarging&gt;</dt>
 *     <dd>This parameter is optional. By default, if the image is smaller
 *         than the specified width and height, the image will be enlarged.
 *         In some circumstances this behaviour is undesirable, and can be
 *         switched off by setting this parameter to "<code>false</code>" so that
 *         images will be reduced in size, but not enlarged. The default is
 *         "<code>true</code>".
 *     </dd>
 *     <dt>&lt;quality&gt;</dt>
 *     <dd>This parameter is optional. By default, the quality uses the
 *         default for the JVM. If it is specified, the proper JPEG quality
 *         compression is used. The range is 0.0 to 1.0, if specified.
 *     </dd>
 *   </dl>
 *
 * @version $Id: ImageReader.java 391248 2006-04-04 08:41:52Z jbq $
 */
final public class ImageReader extends ResourceReader {
    private static final boolean GRAYSCALE_DEFAULT = false;
    private static final boolean ENLARGE_DEFAULT = true;
    private static final boolean FIT_DEFAULT = false;

    /* See http://developer.java.sun.com/developer/bugParade/bugs/4502892.html */
    private static final boolean JVMBugFixed = SystemUtils.isJavaVersionAtLeast(1.4f);

    private int width;
    private int height;
    private float[] scaleColor = new float[3];
    private float[] offsetColor = new float[3];
    private float[] quality = new float[1];

    private boolean enlarge;
    private boolean fitUniform;
    private boolean usePercent;
    private RescaleOp colorFilter;
    private ColorConvertOp grayscaleFilter;

    public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par)
            throws ProcessingException, SAXException, IOException {

        char lastChar;
        String tmpWidth = par.getParameter("width", "0");
        String tmpHeight = par.getParameter("height", "0");

        this.scaleColor[0] = par.getParameterAsFloat("scaleRed", -1.0f);
        this.scaleColor[1] = par.getParameterAsFloat("scaleGreen", -1.0f);
        this.scaleColor[2] = par.getParameterAsFloat("scaleBlue", -1.0f);
        this.offsetColor[0] = par.getParameterAsFloat("offsetRed", 0.0f);
        this.offsetColor[1] = par.getParameterAsFloat("offsetGreen", 0.0f);
        this.offsetColor[2] = par.getParameterAsFloat("offsetBlue", 0.0f);
        this.quality[0] = par.getParameterAsFloat("quality", 0.9f);

        boolean filterColor = false;
        for (int i = 0; i < 3; ++i) {
            if (this.scaleColor[i] != -1.0f) {
                filterColor = true;
            } else {
                this.scaleColor[i] = 1.0f;
            }
            if (this.offsetColor[i] != 0.0f) {
                filterColor = true;
            }
        }

        if (filterColor) {
            this.colorFilter = new RescaleOp(scaleColor, offsetColor, null);
        }

        usePercent = false;
        lastChar = tmpWidth.charAt(tmpWidth.length() - 1);
        if (lastChar == '%') {
            usePercent = true;
            width = Integer.parseInt(tmpWidth.substring(0, tmpWidth.length() - 1));
        } else {
            width = Integer.parseInt(tmpWidth);
        }

        lastChar = tmpHeight.charAt(tmpHeight.length() - 1);
        if (lastChar == '%') {
            usePercent = true;
            height = Integer.parseInt(tmpHeight.substring(0, tmpHeight.length() - 1));
        } else {
            height = Integer.parseInt(tmpHeight);
        }

        if (par.getParameterAsBoolean("grayscale", GRAYSCALE_DEFAULT)) {
            this.grayscaleFilter = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
        }

        this.enlarge = par.getParameterAsBoolean("allow-enlarging", ENLARGE_DEFAULT);
        this.fitUniform = par.getParameterAsBoolean("fit-uniform", FIT_DEFAULT);

        super.setup(resolver, objectModel, src, par);
    }

    protected void setupHeaders() {
        // Reset byte ranges support for dynamic response
        if (byteRanges && hasTransform()) {
            byteRanges = false;
        }

        super.setupHeaders();
    }

    /**
     * @return True if image transform is specified
     */
    private boolean hasTransform() {
        return width > 0 || height > 0 || null != colorFilter || null != grayscaleFilter
                || (this.quality[0] != 0.9f);
    }

    /**
     * Returns the affine transform that implements the scaling.
     * The behavior is the following: if both the new width and height values
     * are positive, the image is rescaled according to these new values and
     * the original aspect ratio is lost.
     * Otherwise, if one of the two parameters is zero or negative, the
     * aspect ratio is maintained and the positive parameter indicates the
     * scaling.
     * If both new values are zero or negative, no scaling takes place (a unit
     * transformation is applied).
     */
    private AffineTransform getTransform(double ow, double oh, double nw, double nh) {
        double wm = 1.0d;
        double hm = 1.0d;

        if (fitUniform) {
            //
            // Compare aspect ratio of image vs. that of the "box"
            // defined by nw and nh
            //
            if (ow / oh > nw / nh) {
                nh = 0; // Original image is proportionately wider than the box,
                        // so scale to fit width
            } else {
                nw = 0; // Scale to fit height
            }
        }

        if (nw > 0) {
            wm = nw / ow;
            if (nh > 0) {
                hm = nh / oh;
            } else {
                hm = wm;
            }
        } else {
            if (nh > 0) {
                hm = nh / oh;
                wm = hm;
            }
        }

        if (!enlarge) {
            if ((nw > ow && nh <= 0) || (nh > oh && nw <= 0)) {
                wm = 1.0d;
                hm = 1.0d;
            } else if (nw > ow) {
                wm = 1.0d;
            } else if (nh > oh) {
                hm = 1.0d;
            }
        }
        return new AffineTransform(wm, 0.0d, 0.0d, hm, 0.0d, 0.0d);
    }

    protected byte[] readFully(InputStream in) throws IOException {
        byte tmpbuffer[] = new byte[4096];
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i;
        while (-1 != (i = in.read(tmpbuffer))) {
            baos.write(tmpbuffer, 0, i);
        }
        baos.flush();
        return baos.toByteArray();
    }

    protected void processStream(InputStream inputStream) throws IOException, ProcessingException {
        if (hasTransform()) {
            if (getLogger().isDebugEnabled()) {
                getLogger().debug("image " + ((width == 0) ? "?" : Integer.toString(width)) + "x"
                        + ((height == 0) ? "?" : Integer.toString(height)) + " expires: " + expires);
            }

            /*
             * NOTE (SM):
             * Due to Bug Id 4502892 (which is found in *all* JVM implementations from
             * 1.2.x and 1.3.x on all OS!), we must buffer the JPEG generation to avoid
             * that connection resetting by the peer (user pressing the stop button,
             * for example) crashes the entire JVM (yes, dude, the bug is *that* nasty
             * since it happens in JPEG routines which are native!)
             * I'm perfectly aware of the huge memory problems that this causes (almost
             * doubling memory consuption for each image and making the GC work twice
             * as hard) but it's *far* better than restarting the JVM every 2 minutes
             * (since this is the average experience for image-intensive web application
             * such as an image gallery).
             * Please, go to the <a href="http://developer.java.sun.com/developer/bugParade/bugs/4502892.html">Sun Developers Connection</a>
             * and vote this BUG as the one you would like fixed sooner rather than
             * later and all this hack will automagically go away.
             * Many deep thanks to Michael Hartle <mhartle@hartle-klug.com> for tracking
             * this down and suggesting the workaround.
             *
             * UPDATE (SM):
             * This appears to be fixed on JDK 1.4
             */

            try {
                byte content[] = readFully(inputStream);
                ImageIcon icon = new ImageIcon(content);
                BufferedImage original = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(),
                        BufferedImage.TYPE_INT_RGB);
                BufferedImage currentImage = original;
                currentImage.getGraphics().drawImage(icon.getImage(), 0, 0, null);

                if (width > 0 || height > 0) {
                    double ow = icon.getImage().getWidth(null);
                    double oh = icon.getImage().getHeight(null);

                    if (usePercent) {
                        if (width > 0) {
                            width = Math.round((int) (ow * width) / 100);
                        }
                        if (height > 0) {
                            height = Math.round((int) (oh * height) / 100);
                        }
                    }

                    AffineTransformOp filter = new AffineTransformOp(getTransform(ow, oh, width, height),
                            AffineTransformOp.TYPE_BILINEAR);
                    WritableRaster scaledRaster = filter.createCompatibleDestRaster(currentImage.getRaster());

                    filter.filter(currentImage.getRaster(), scaledRaster);

                    currentImage = new BufferedImage(original.getColorModel(), scaledRaster, true, null);
                }

                if (null != grayscaleFilter) {
                    grayscaleFilter.filter(currentImage, currentImage);
                }

                if (null != colorFilter) {
                    colorFilter.filter(currentImage, currentImage);
                }

                // JVM Bug handling
                if (JVMBugFixed) {
                    JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
                    JPEGEncodeParam p = encoder.getDefaultJPEGEncodeParam(currentImage);
                    p.setQuality(this.quality[0], true);
                    encoder.setJPEGEncodeParam(p);
                    encoder.encode(currentImage);
                } else {
                    ByteArrayOutputStream bstream = new ByteArrayOutputStream();
                    JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(bstream);
                    JPEGEncodeParam p = encoder.getDefaultJPEGEncodeParam(currentImage);
                    p.setQuality(this.quality[0], true);
                    encoder.setJPEGEncodeParam(p);
                    encoder.encode(currentImage);
                    out.write(bstream.toByteArray());
                }

                out.flush();
            } catch (ImageFormatException e) {
                throw new ProcessingException(
                        "Error reading the image. " + "Note that only JPEG images are currently supported.");
            } finally {
                // Bugzilla Bug 25069, close inputStream in finally block
                // this will close inputStream even if processStream throws
                // an exception
                inputStream.close();
            }
        } else {
            // only read the resource - no modifications requested
            if (getLogger().isDebugEnabled()) {
                getLogger().debug("passing original resource");
            }
            super.processStream(inputStream);
        }
    }

    /**
     * Generate the unique key.
     * This key must be unique inside the space of this component.
     *
     * @return The generated key consists of the src and width and height,
     *         and the color transform parameters
    */
    public Serializable getKey() {
        return super.getKey().toString() + ':' + this.fitUniform + ':' + this.enlarge + ':' + this.width + ':'
                + this.height + ":" + this.scaleColor[0] + ":" + this.scaleColor[1] + ":" + this.scaleColor[2] + ":"
                + this.offsetColor[0] + ":" + this.offsetColor[1] + ":" + this.offsetColor[2] + ":"
                + this.quality[0] + ":" + (this.grayscaleFilter == null ? "color" : "bw");
    }

    public void recycle() {
        super.recycle();
        this.colorFilter = null;
        this.grayscaleFilter = null;
    }
}