001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.reading;
018:
019: import java.awt.color.ColorSpace;
020: import java.awt.geom.AffineTransform;
021: import java.awt.image.AffineTransformOp;
022: import java.awt.image.BufferedImage;
023: import java.awt.image.ColorConvertOp;
024: import java.awt.image.RescaleOp;
025: import java.awt.image.WritableRaster;
026: import java.io.ByteArrayOutputStream;
027: import java.io.IOException;
028: import java.io.InputStream;
029: import java.io.Serializable;
030: import java.util.Map;
031: import javax.swing.ImageIcon;
032:
033: import org.apache.avalon.framework.parameters.Parameters;
034: import org.apache.cocoon.ProcessingException;
035: import org.apache.cocoon.environment.SourceResolver;
036: import org.apache.commons.lang.SystemUtils;
037: import org.xml.sax.SAXException;
038:
039: import com.sun.image.codec.jpeg.ImageFormatException;
040: import com.sun.image.codec.jpeg.JPEGCodec;
041: import com.sun.image.codec.jpeg.JPEGEncodeParam;
042: import com.sun.image.codec.jpeg.JPEGImageEncoder;
043:
044: /**
045: * The <code>ImageReader</code> component is used to serve binary image data
046: * in a sitemap pipeline. It makes use of HTTP Headers to determine if
047: * the requested resource should be written to the <code>OutputStream</code>
048: * or if it can signal that it hasn't changed.
049: *
050: * Parameters:
051: * <dl>
052: * <dt><width></dt>
053: * <dd> This parameter is optional. When specified, it determines the
054: * width of the binary image.
055: * If no height parameter is specified, the aspect ratio
056: * of the image is kept. The parameter may be expressed as an int or a percentage.
057: * </dd>
058: * <dt><height></dt>
059: * <dd> This parameter is optional. When specified, it determines the
060: * height of the binary image.
061: * If no width parameter is specified, the aspect ratio
062: * of the image is kept. The parameter may be expressed as an int or a percentage.
063: * </dd>
064: * <dt><scale(Red|Green|Blue)></dt>
065: * <dd>This parameter is optional. When specified it will cause the
066: * specified color component in the image to be multiplied by the
067: * specified floating point value.
068: * </dd>
069: * <dt><offset(Red|Green|Blue)></dt>
070: * <dd>This parameter is optional. When specified it will cause the
071: * specified color component in the image to be incremented by the
072: * specified floating point value.
073: * </dd>
074: * <dt><grayscale></dt>
075: * <dd>This parameter is optional. When specified and set to true it
076: * will cause each image pixel to be normalized. Default is "false".
077: * </dd>
078: * <dt><allow-enlarging></dt>
079: * <dd>This parameter is optional. By default, if the image is smaller
080: * than the specified width and height, the image will be enlarged.
081: * In some circumstances this behaviour is undesirable, and can be
082: * switched off by setting this parameter to "<code>false</code>" so that
083: * images will be reduced in size, but not enlarged. The default is
084: * "<code>true</code>".
085: * </dd>
086: * <dt><quality></dt>
087: * <dd>This parameter is optional. By default, the quality uses the
088: * default for the JVM. If it is specified, the proper JPEG quality
089: * compression is used. The range is 0.0 to 1.0, if specified.
090: * </dd>
091: * </dl>
092: *
093: * @version $Id: ImageReader.java 433543 2006-08-22 06:22:54Z crossley $
094: */
095: final public class ImageReader extends ResourceReader {
096: private static final boolean GRAYSCALE_DEFAULT = false;
097: private static final boolean ENLARGE_DEFAULT = true;
098: private static final boolean FIT_DEFAULT = false;
099:
100: /* See http://developer.java.sun.com/developer/bugParade/bugs/4502892.html */
101: private static final boolean JVMBugFixed = SystemUtils
102: .isJavaVersionAtLeast(1.4f);
103:
104: private int width;
105: private int height;
106: private float[] scaleColor = new float[3];
107: private float[] offsetColor = new float[3];
108: private float[] quality = new float[1];
109:
110: private boolean enlarge;
111: private boolean fitUniform;
112: private boolean usePercent;
113: private RescaleOp colorFilter;
114: private ColorConvertOp grayscaleFilter;
115:
116: public void setup(SourceResolver resolver, Map objectModel,
117: String src, Parameters par) throws ProcessingException,
118: SAXException, IOException {
119:
120: char lastChar;
121: String tmpWidth = par.getParameter("width", "0");
122: String tmpHeight = par.getParameter("height", "0");
123:
124: this .scaleColor[0] = par.getParameterAsFloat("scaleRed", -1.0f);
125: this .scaleColor[1] = par.getParameterAsFloat("scaleGreen",
126: -1.0f);
127: this .scaleColor[2] = par
128: .getParameterAsFloat("scaleBlue", -1.0f);
129: this .offsetColor[0] = par
130: .getParameterAsFloat("offsetRed", 0.0f);
131: this .offsetColor[1] = par.getParameterAsFloat("offsetGreen",
132: 0.0f);
133: this .offsetColor[2] = par.getParameterAsFloat("offsetBlue",
134: 0.0f);
135: this .quality[0] = par.getParameterAsFloat("quality", 0.9f);
136:
137: boolean filterColor = false;
138: for (int i = 0; i < 3; ++i) {
139: if (this .scaleColor[i] != -1.0f) {
140: filterColor = true;
141: } else {
142: this .scaleColor[i] = 1.0f;
143: }
144: if (this .offsetColor[i] != 0.0f) {
145: filterColor = true;
146: }
147: }
148:
149: if (filterColor) {
150: this .colorFilter = new RescaleOp(scaleColor, offsetColor,
151: null);
152: }
153:
154: usePercent = false;
155: lastChar = tmpWidth.charAt(tmpWidth.length() - 1);
156: if (lastChar == '%') {
157: usePercent = true;
158: width = Integer.parseInt(tmpWidth.substring(0, tmpWidth
159: .length() - 1));
160: } else {
161: width = Integer.parseInt(tmpWidth);
162: }
163:
164: lastChar = tmpHeight.charAt(tmpHeight.length() - 1);
165: if (lastChar == '%') {
166: usePercent = true;
167: height = Integer.parseInt(tmpHeight.substring(0, tmpHeight
168: .length() - 1));
169: } else {
170: height = Integer.parseInt(tmpHeight);
171: }
172:
173: if (par.getParameterAsBoolean("grayscale", GRAYSCALE_DEFAULT)) {
174: this .grayscaleFilter = new ColorConvertOp(ColorSpace
175: .getInstance(ColorSpace.CS_GRAY), null);
176: }
177:
178: this .enlarge = par.getParameterAsBoolean("allow-enlarging",
179: ENLARGE_DEFAULT);
180: this .fitUniform = par.getParameterAsBoolean("fit-uniform",
181: FIT_DEFAULT);
182:
183: super .setup(resolver, objectModel, src, par);
184: }
185:
186: protected void setupHeaders() {
187: // Reset byte ranges support for dynamic response
188: if (byteRanges && hasTransform()) {
189: byteRanges = false;
190: }
191:
192: super .setupHeaders();
193: }
194:
195: /**
196: * @return True if image transform is specified
197: */
198: private boolean hasTransform() {
199: return width > 0 || height > 0 || null != colorFilter
200: || null != grayscaleFilter || (this .quality[0] != 0.9f);
201: }
202:
203: /**
204: * Returns the affine transform that implements the scaling.
205: * The behavior is the following: if both the new width and height values
206: * are positive, the image is rescaled according to these new values and
207: * the original aspect ratio is lost.
208: * Otherwise, if one of the two parameters is zero or negative, the
209: * aspect ratio is maintained and the positive parameter indicates the
210: * scaling.
211: * If both new values are zero or negative, no scaling takes place (a unit
212: * transformation is applied).
213: */
214: private AffineTransform getTransform(double ow, double oh,
215: double nw, double nh) {
216: double wm = 1.0d;
217: double hm = 1.0d;
218:
219: if (fitUniform) {
220: //
221: // Compare aspect ratio of image vs. that of the "box"
222: // defined by nw and nh
223: //
224: if (ow / oh > nw / nh) {
225: nh = 0; // Original image is proportionately wider than the box,
226: // so scale to fit width
227: } else {
228: nw = 0; // Scale to fit height
229: }
230: }
231:
232: if (nw > 0) {
233: wm = nw / ow;
234: if (nh > 0) {
235: hm = nh / oh;
236: } else {
237: hm = wm;
238: }
239: } else {
240: if (nh > 0) {
241: hm = nh / oh;
242: wm = hm;
243: }
244: }
245:
246: if (!enlarge) {
247: if ((nw > ow && nh <= 0) || (nh > oh && nw <= 0)) {
248: wm = 1.0d;
249: hm = 1.0d;
250: } else if (nw > ow) {
251: wm = 1.0d;
252: } else if (nh > oh) {
253: hm = 1.0d;
254: }
255: }
256: return new AffineTransform(wm, 0.0d, 0.0d, hm, 0.0d, 0.0d);
257: }
258:
259: protected byte[] readFully(InputStream in) throws IOException {
260: byte tmpbuffer[] = new byte[4096];
261: ByteArrayOutputStream baos = new ByteArrayOutputStream();
262: int i;
263: while (-1 != (i = in.read(tmpbuffer))) {
264: baos.write(tmpbuffer, 0, i);
265: }
266: baos.flush();
267: return baos.toByteArray();
268: }
269:
270: protected void processStream(InputStream inputStream)
271: throws IOException, ProcessingException {
272: if (hasTransform()) {
273: if (getLogger().isDebugEnabled()) {
274: getLogger().debug(
275: "image "
276: + ((width == 0) ? "?" : Integer
277: .toString(width))
278: + "x"
279: + ((height == 0) ? "?" : Integer
280: .toString(height))
281: + " expires: " + expires);
282: }
283:
284: /*
285: * NOTE (SM):
286: * Due to Bug Id 4502892 (which is found in *all* JVM implementations from
287: * 1.2.x and 1.3.x on all OS!), we must buffer the JPEG generation to avoid
288: * that connection resetting by the peer (user pressing the stop button,
289: * for example) crashes the entire JVM (yes, dude, the bug is *that* nasty
290: * since it happens in JPEG routines which are native!)
291: * I'm perfectly aware of the huge memory problems that this causes (almost
292: * doubling memory consumption for each image and making the GC work twice
293: * as hard) but it's *far* better than restarting the JVM every 2 minutes
294: * (since this is the average experience for image-intensive web application
295: * such as an image gallery).
296: * Please, go to the <a href="http://developer.java.sun.com/developer/bugParade/bugs/4502892.html">Sun Developers Connection</a>
297: * and vote this BUG as the one you would like fixed sooner rather than
298: * later and all this hack will automagically go away.
299: * Many deep thanks to Michael Hartle <mhartle@hartle-klug.com> for tracking
300: * this down and suggesting the workaround.
301: *
302: * UPDATE (SM):
303: * This appears to be fixed on JDK 1.4
304: */
305:
306: try {
307: byte content[] = readFully(inputStream);
308: ImageIcon icon = new ImageIcon(content);
309: BufferedImage original = new BufferedImage(icon
310: .getIconWidth(), icon.getIconHeight(),
311: BufferedImage.TYPE_INT_RGB);
312: BufferedImage currentImage = original;
313: currentImage.getGraphics().drawImage(icon.getImage(),
314: 0, 0, null);
315:
316: if (width > 0 || height > 0) {
317: double ow = icon.getImage().getWidth(null);
318: double oh = icon.getImage().getHeight(null);
319:
320: if (usePercent) {
321: if (width > 0) {
322: width = Math
323: .round((int) (ow * width) / 100);
324: }
325: if (height > 0) {
326: height = Math
327: .round((int) (oh * height) / 100);
328: }
329: }
330:
331: AffineTransformOp filter = new AffineTransformOp(
332: getTransform(ow, oh, width, height),
333: AffineTransformOp.TYPE_BILINEAR);
334: WritableRaster scaledRaster = filter
335: .createCompatibleDestRaster(currentImage
336: .getRaster());
337:
338: filter.filter(currentImage.getRaster(),
339: scaledRaster);
340:
341: currentImage = new BufferedImage(original
342: .getColorModel(), scaledRaster, true, null);
343: }
344:
345: if (null != grayscaleFilter) {
346: grayscaleFilter.filter(currentImage, currentImage);
347: }
348:
349: if (null != colorFilter) {
350: colorFilter.filter(currentImage, currentImage);
351: }
352:
353: // JVM Bug handling
354: if (JVMBugFixed) {
355: JPEGImageEncoder encoder = JPEGCodec
356: .createJPEGEncoder(out);
357: JPEGEncodeParam p = encoder
358: .getDefaultJPEGEncodeParam(currentImage);
359: p.setQuality(this .quality[0], true);
360: encoder.setJPEGEncodeParam(p);
361: encoder.encode(currentImage);
362: } else {
363: ByteArrayOutputStream bstream = new ByteArrayOutputStream();
364: JPEGImageEncoder encoder = JPEGCodec
365: .createJPEGEncoder(bstream);
366: JPEGEncodeParam p = encoder
367: .getDefaultJPEGEncodeParam(currentImage);
368: p.setQuality(this .quality[0], true);
369: encoder.setJPEGEncodeParam(p);
370: encoder.encode(currentImage);
371: out.write(bstream.toByteArray());
372: }
373:
374: out.flush();
375: } catch (ImageFormatException e) {
376: throw new ProcessingException(
377: "Error reading the image. "
378: + "Note that only JPEG images are currently supported.");
379: } finally {
380: // Bugzilla Bug 25069, close inputStream in finally block
381: // this will close inputStream even if processStream throws
382: // an exception
383: inputStream.close();
384: }
385: } else {
386: // only read the resource - no modifications requested
387: if (getLogger().isDebugEnabled()) {
388: getLogger().debug("passing original resource");
389: }
390: super .processStream(inputStream);
391: }
392: }
393:
394: /**
395: * Generate the unique key.
396: * This key must be unique inside the space of this component.
397: *
398: * @return The generated key consists of the src and width and height,
399: * and the color transform parameters
400: */
401: public Serializable getKey() {
402: return super .getKey().toString() + ':' + this .fitUniform + ':'
403: + this .enlarge + ':' + this .width + ':' + this .height
404: + ":" + this .scaleColor[0] + ":" + this .scaleColor[1]
405: + ":" + this .scaleColor[2] + ":" + this .offsetColor[0]
406: + ":" + this .offsetColor[1] + ":" + this .offsetColor[2]
407: + ":" + this .quality[0] + ":"
408: + (this .grayscaleFilter == null ? "color" : "bw");
409: }
410:
411: public void recycle() {
412: super.recycle();
413: this.colorFilter = null;
414: this.grayscaleFilter = null;
415: }
416: }
|