blob: 8f23255215c457311b837d188e31324697255318 [file] [log] [blame]
/*
* 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 org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.environment.SourceResolver;
import com.sun.image.codec.jpeg.ImageFormatException;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGDecodeParam;
import com.sun.image.codec.jpeg.JPEGImageDecoder;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import org.xml.sax.SAXException;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Map;
/**
* 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 image that should be served.
* </dd>
* <dt>&lt;height&gt;</dt>
* <dd>This parameter is optional. When specified it determines the height
* of the image that should be served.
* </dd>
* </dl>
*
* @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
* @author <a href="mailto:stephan@apache.org">Stephan Michels</a>
* @author <a href="mailto:tcurdt@apache.org">Torsten Curdt</a>
* @version CVS $Id: ImageReader.java,v 1.6 2004/03/05 13:02:57 bdelacretaz Exp $
*/
final public class ImageReader extends ResourceReader {
private int width;
private int height;
private boolean enlarge;
private final static String ENLARGE_DEFAULT = "true";
public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par)
throws ProcessingException, SAXException, IOException {
super.setup(resolver, objectModel, src, par);
width = par.getParameterAsInteger("width", 0);
height = par.getParameterAsInteger("height", 0);
String enlargePar = par.getParameter("allow-enlarging", ENLARGE_DEFAULT);
if ("true".equalsIgnoreCase(enlargePar) || "yes".equalsIgnoreCase(enlargePar)){
enlarge = true;
} else {
enlarge = false;
}
}
/**
* 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 ration 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 (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 void processStream() throws IOException, ProcessingException {
if (width > 0 || height > 0) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("image " + ((width==0)?"?":Integer.toString(width))
+ "x" + ((height==0)?"?":Integer.toString(height))
+ " expires: " + expires);
}
// since we create the image on the fly
response.setHeader("Accept-Ranges", "none");
/**
* 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 {
JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(inputStream);
Raster original = decoder.decodeAsRaster();
JPEGDecodeParam decodeParam = decoder.getJPEGDecodeParam();
double ow = decodeParam.getWidth();
double oh = decodeParam.getHeight();
AffineTransformOp filter = new AffineTransformOp(getTransform(ow, oh, width, height), AffineTransformOp.TYPE_BILINEAR);
WritableRaster scaled = filter.createCompatibleDestRaster(original);
filter.filter(original, scaled);
if (!handleJVMBug()) {
if (getLogger().isDebugEnabled()) {
getLogger().debug( "No need to handle JVM bug" );
}
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
encoder.encode(scaled);
} else {
if (getLogger().isDebugEnabled()) {
getLogger().debug( "Need to handle JVM bug" );
}
ByteArrayOutputStream bstream = new ByteArrayOutputStream();
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(bstream);
encoder.encode(scaled);
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();
}
}
/**
* Generate the unique key.
* This key must be unique inside the space of this component.
*
* @return The generated key consists from src and width and height
* parameters
*/
public Serializable getKey() {
if (width > 0 || height > 0) {
return this.inputSource.getURI() + ':' + this.width + ':' + this.height;
} else {
return super.getKey();
}
}
/**
* Determine if workaround for Bug Id 4502892 is neccessary.
* This method assumes that Bug is present if
* java.version is undeterminable, and for java.version
* 1.1, 1.2, 1.3, all other java.version do not need the Bug handling
*
* @return true if we should handle the JVM bug, else false
*/
protected boolean handleJVMBug() {
// java.version=1.4.0
String java_version = System.getProperty( "java.version", "0.0.0" );
boolean handleJVMBug = true;
char major = java_version.charAt(0);
char minor = java_version.charAt(2);
// make 0.0, 1.1, 1.2, 1.3 handleJVMBug = true
if (major == '0' || major == '1') {
if (minor == '0' || minor == '1' || minor == '2' || minor == '3') {
handleJVMBug = true;
} else {
handleJVMBug = false;
}
} else {
handleJVMBug = true;
}
if (getLogger().isDebugEnabled()) {
getLogger().debug( "Running java.version " + String.valueOf(java_version) +
" need to handle JVM bug " + String.valueOf(handleJVMBug) );
}
return handleJVMBug;
}
}