KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > cocoon > reading > ImageReader


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

16 package org.apache.cocoon.reading;
17
18 import java.awt.color.ColorSpace JavaDoc;
19 import java.awt.geom.AffineTransform JavaDoc;
20 import java.awt.image.AffineTransformOp JavaDoc;
21 import java.awt.image.BufferedImage JavaDoc;
22 import java.awt.image.ColorConvertOp JavaDoc;
23 import java.awt.image.RescaleOp JavaDoc;
24 import java.awt.image.WritableRaster JavaDoc;
25 import java.io.ByteArrayOutputStream JavaDoc;
26 import java.io.IOException JavaDoc;
27 import java.io.InputStream JavaDoc;
28 import java.io.Serializable JavaDoc;
29 import java.util.Map JavaDoc;
30
31 import org.apache.avalon.framework.parameters.Parameters;
32 import org.apache.cocoon.ProcessingException;
33 import org.apache.cocoon.environment.SourceResolver;
34 import org.apache.cocoon.reading.ResourceReader;
35 import org.apache.commons.lang.SystemUtils;
36 import org.xml.sax.SAXException JavaDoc;
37
38 import com.sun.image.codec.jpeg.ImageFormatException;
39 import com.sun.image.codec.jpeg.JPEGCodec;
40 import com.sun.image.codec.jpeg.JPEGDecodeParam;
41 import com.sun.image.codec.jpeg.JPEGEncodeParam;
42 import com.sun.image.codec.jpeg.JPEGImageDecoder;
43 import com.sun.image.codec.jpeg.JPEGImageEncoder;
44
45 /**
46  * The <code>ImageReader</code> component is used to serve binary image data
47  * in a sitemap pipeline. It makes use of HTTP Headers to determine if
48  * the requested resource should be written to the <code>OutputStream</code>
49  * or if it can signal that it hasn't changed.
50  *
51  * Parameters:
52  * <dl>
53  * <dt>&lt;width&gt;</dt>
54  * <dd> This parameter is optional. When specified, it determines the
55  * width of the binary image.
56  * If no height parameter is specified, the aspect ratio
57  * of the image is kept. The parameter may be expressed as an int or a percentage.
58  * </dd>
59  * <dt>&lt;height&gt;</dt>
60  * <dd> This parameter is optional. When specified, it determines the
61  * height of the binary image.
62  * If no width parameter is specified, the aspect ratio
63  * of the image is kept. The parameter may be expressed as an int or a percentage.
64  * </dd>
65  * <dt>&lt;scale(Red|Green|Blue)&gt;</dt>
66  * <dd>This parameter is optional. When specified it will cause the
67  * specified color component in the image to be multiplied by the
68  * specified floating point value.
69  * </dd>
70  * <dt>&lt;offset(Red|Green|Blue)&gt;</dt>
71  * <dd>This parameter is optional. When specified it will cause the
72  * specified color component in the image to be incremented by the
73  * specified floating point value.
74  * </dd>
75  * <dt>&lt;grayscale&gt;</dt>
76  * <dd>This parameter is optional. When specified and set to true it
77  * will cause each image pixel to be normalized. Default is "false".
78  * </dd>
79  * <dt>&lt;allow-enlarging&gt;</dt>
80  * <dd>This parameter is optional. By default, if the image is smaller
81  * than the specified width and height, the image will be enlarged.
82  * In some circumstances this behaviour is undesirable, and can be
83  * switched off by setting this parameter to "<code>false</code>" so that
84  * images will be reduced in size, but not enlarged. The default is
85  * "<code>true</code>".
86  * </dd>
87  * <dt>&lt;quality&gt;</dt>
88  * <dd>This parameter is optional. By default, the quality uses the
89  * default for the JVM. If it is specified, the proper JPEG quality
90  * compression is used. The range is 0.0 to 1.0, if specified.
91  * </dd>
92  * </dl>
93  *
94  * @author <a HREF="mailto:stefano@apache.org">Stefano Mazzocchi</a>
95  * @author <a HREF="mailto:stephan@apache.org">Stephan Michels</a>
96  * @author <a HREF="mailto:tcurdt@apache.org">Torsten Curdt</a>
97  * @author <a HREF="mailto:eric@plauditdesign.com">Eric Caron</a>
98  * @version CVS $Id: ImageReader.java 289503 2005-09-16 12:04:17Z jheymans $
99  */

100 final public class ImageReader extends ResourceReader {
101     private static final boolean GRAYSCALE_DEFAULT = false;
102     private static final boolean ENLARGE_DEFAULT = true;
103     private static final boolean FIT_DEFAULT = false;
104
105     /* See http://developer.java.sun.com/developer/bugParade/bugs/4502892.html */
106     private static final boolean JVMBugFixed = SystemUtils.isJavaVersionAtLeast(1.4f);
107
108     private int width;
109     private int height;
110     private float[] scaleColor = new float[3];
111     private float[] offsetColor = new float[3];
112     private float[] quality = new float[1];
113
114     private boolean enlarge;
115     private boolean fitUniform;
116     private boolean usePercent;
117     private RescaleOp JavaDoc colorFilter;
118     private ColorConvertOp JavaDoc grayscaleFilter;
119
120
121     public void setup(SourceResolver resolver, Map JavaDoc objectModel, String JavaDoc src, Parameters par)
122     throws ProcessingException, SAXException JavaDoc, IOException JavaDoc {
123
124         char lastChar;
125         String JavaDoc tmpWidth = par.getParameter("width", "0");
126         String JavaDoc tmpHeight = par.getParameter("height", "0");
127
128         this.scaleColor[0] = par.getParameterAsFloat("scaleRed", -1.0f);
129         this.scaleColor[1] = par.getParameterAsFloat("scaleGreen", -1.0f);
130         this.scaleColor[2] = par.getParameterAsFloat("scaleBlue", -1.0f);
131         this.offsetColor[0] = par.getParameterAsFloat("offsetRed", 0.0f);
132         this.offsetColor[1] = par.getParameterAsFloat("offsetGreen", 0.0f);
133         this.offsetColor[2] = par.getParameterAsFloat("offsetBlue", 0.0f);
134         this.quality[0] = par.getParameterAsFloat("quality", 0.9f);
135
136         boolean filterColor = false;
137         for (int i = 0; i < 3; ++i) {
138             if (this.scaleColor[i] != -1.0f) {
139                 filterColor = true;
140             } else {
141                 this.scaleColor[i] = 1.0f;
142             }
143             if (this.offsetColor[i] != 0.0f) {
144                 filterColor = true;
145             }
146         }
147
148         if (filterColor) {
149             this.colorFilter = new RescaleOp JavaDoc(scaleColor, offsetColor, null);
150         }
151
152         usePercent = false;
153         lastChar = tmpWidth.charAt(tmpWidth.length() - 1);
154         if (lastChar == '%') {
155             usePercent = true;
156             width = Integer.parseInt(tmpWidth.substring(0, tmpWidth.length() - 1));
157         } else {
158             width = Integer.parseInt(tmpWidth);
159         }
160
161         lastChar = tmpHeight.charAt(tmpHeight.length() - 1);
162         if(lastChar == '%') {
163             usePercent = true;
164             height = Integer.parseInt(tmpHeight.substring(0, tmpHeight.length() - 1));
165         } else {
166             height = Integer.parseInt(tmpHeight);
167         }
168         
169         if (par.getParameterAsBoolean("grayscale", GRAYSCALE_DEFAULT)) {
170             this.grayscaleFilter = new ColorConvertOp JavaDoc(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
171         }
172
173         this.enlarge = par.getParameterAsBoolean("allow-enlarging", ENLARGE_DEFAULT);
174         this.fitUniform = par.getParameterAsBoolean("fit-uniform", FIT_DEFAULT);
175
176         super.setup(resolver, objectModel, src, par);
177     }
178
179     protected void setupHeaders() {
180         // Reset byte ranges support for dynamic response
181
if (byteRanges && hasTransform()) {
182             byteRanges = false;
183         }
184
185         super.setupHeaders();
186     }
187
188     /**
189      * @return True if image transform is specified
190      */

191     private boolean hasTransform() {
192         return width > 0 || height > 0 || null != colorFilter || null != grayscaleFilter || (this.quality[0] != 0.9f);
193     }
194
195     /**
196      * Returns the affine transform that implements the scaling.
197      * The behavior is the following: if both the new width and height values
198      * are positive, the image is rescaled according to these new values and
199      * the original aspect ratio is lost.
200      * Otherwise, if one of the two parameters is zero or negative, the
201      * aspect ratio is maintained and the positive parameter indicates the
202      * scaling.
203      * If both new values are zero or negative, no scaling takes place (a unit
204      * transformation is applied).
205      */

206     private AffineTransform JavaDoc getTransform(double ow, double oh, double nw, double nh) {
207         double wm = 1.0d;
208         double hm = 1.0d;
209
210         if (fitUniform) {
211             //
212
// Compare aspect ratio of image vs. that of the "box"
213
// defined by nw and nh
214
//
215
if (ow/oh > nw/nh) {
216                 nh = 0; // Original image is proportionately wider than the box,
217
// so scale to fit width
218
} else {
219                 nw = 0; // Scale to fit height
220
}
221         }
222
223         if (nw > 0) {
224             wm = nw / ow;
225             if (nh > 0) {
226                 hm = nh / oh;
227             } else {
228                 hm = wm;
229             }
230         } else {
231             if (nh > 0) {
232                 hm = nh / oh;
233                 wm = hm;
234             }
235         }
236
237         if (!enlarge) {
238             if ((nw > ow && nh <= 0) || (nh > oh && nw <=0)) {
239                 wm = 1.0d;
240                 hm = 1.0d;
241             } else if (nw > ow) {
242                 wm = 1.0d;
243             } else if (nh > oh) {
244                 hm = 1.0d;
245             }
246         }
247         return new AffineTransform JavaDoc(wm, 0.0d, 0.0d, hm, 0.0d, 0.0d);
248     }
249
250     protected void processStream(InputStream JavaDoc inputStream) throws IOException JavaDoc, ProcessingException {
251         if (hasTransform()) {
252             if (getLogger().isDebugEnabled()) {
253                 getLogger().debug("image " + ((width == 0) ? "?" : Integer.toString(width))
254                                   + "x" + ((height == 0) ? "?" : Integer.toString(height))
255                                   + " expires: " + expires);
256             }
257
258             /*
259              * NOTE (SM):
260              * Due to Bug Id 4502892 (which is found in *all* JVM implementations from
261              * 1.2.x and 1.3.x on all OS!), we must buffer the JPEG generation to avoid
262              * that connection resetting by the peer (user pressing the stop button,
263              * for example) crashes the entire JVM (yes, dude, the bug is *that* nasty
264              * since it happens in JPEG routines which are native!)
265              * I'm perfectly aware of the huge memory problems that this causes (almost
266              * doubling memory consuption for each image and making the GC work twice
267              * as hard) but it's *far* better than restarting the JVM every 2 minutes
268              * (since this is the average experience for image-intensive web application
269              * such as an image gallery).
270              * Please, go to the <a HREF="http://developer.java.sun.com/developer/bugParade/bugs/4502892.html">Sun Developers Connection</a>
271              * and vote this BUG as the one you would like fixed sooner rather than
272              * later and all this hack will automagically go away.
273              * Many deep thanks to Michael Hartle <mhartle@hartle-klug.com> for tracking
274              * this down and suggesting the workaround.
275              *
276              * UPDATE (SM):
277              * This appears to be fixed on JDK 1.4
278              */

279
280             try {
281                 JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(inputStream);
282                 BufferedImage JavaDoc original = decoder.decodeAsBufferedImage();
283                 BufferedImage JavaDoc currentImage = original;
284
285                 if (width > 0 || height > 0) {
286                     JPEGDecodeParam decodeParam = decoder.getJPEGDecodeParam();
287                     double ow = decodeParam.getWidth();
288                     double oh = decodeParam.getHeight();
289
290                     if (usePercent == true) {
291                         if (width > 0) {
292                             width = Math.round((int)(ow * width) / 100);
293                         }
294                         if (height > 0) {
295                             height = Math.round((int)(oh * height) / 100);
296                         }
297                     }
298
299                     AffineTransformOp JavaDoc filter = new AffineTransformOp JavaDoc(getTransform(ow, oh, width, height), AffineTransformOp.TYPE_BILINEAR);
300                     WritableRaster JavaDoc scaledRaster = filter.createCompatibleDestRaster(currentImage.getRaster());
301
302                     filter.filter(currentImage.getRaster(), scaledRaster);
303
304                     currentImage = new BufferedImage JavaDoc(original.getColorModel(), scaledRaster, true, null);
305                 }
306
307                 if (null != grayscaleFilter) {
308                     grayscaleFilter.filter(currentImage, currentImage);
309                 }
310
311                 if (null != colorFilter) {
312                     colorFilter.filter(currentImage, currentImage);
313                 }
314
315                 // JVM Bug handling
316
if (JVMBugFixed) {
317                     JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
318                     JPEGEncodeParam p = encoder.getDefaultJPEGEncodeParam(currentImage);
319                     p.setQuality(this.quality[0], true);
320                     encoder.setJPEGEncodeParam(p);
321                     encoder.encode(currentImage);
322                 } else {
323                     ByteArrayOutputStream JavaDoc bstream = new ByteArrayOutputStream JavaDoc();
324                     JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(bstream);
325                     JPEGEncodeParam p = encoder.getDefaultJPEGEncodeParam(currentImage);
326                     p.setQuality(this.quality[0], true);
327                     encoder.setJPEGEncodeParam(p);
328                     encoder.encode(currentImage);
329                     out.write(bstream.toByteArray());
330                 }
331
332                 out.flush();
333             } catch (ImageFormatException e) {
334                 throw new ProcessingException("Error reading the image. " +
335                                               "Note that only JPEG images are currently supported.");
336             } finally {
337               // Bugzilla Bug 25069, close inputStream in finally block
338
// this will close inputStream even if processStream throws
339
// an exception
340
inputStream.close();
341             }
342         } else {
343             // only read the resource - no modifications requested
344
if (getLogger().isDebugEnabled()) {
345                 getLogger().debug("passing original resource");
346             }
347             super.processStream(inputStream);
348         }
349     }
350
351     /**
352      * Generate the unique key.
353      * This key must be unique inside the space of this component.
354      *
355      * @return The generated key consists of the src and width and height, and the color transform
356      * parameters
357     */

358     public Serializable JavaDoc getKey() {
359         return this.inputSource.getURI()
360                 + ':' + this.width
361                 + ':' + this.height
362                 + ":" + this.scaleColor[0]
363                 + ":" + this.scaleColor[1]
364                 + ":" + this.scaleColor[2]
365                 + ":" + this.offsetColor[0]
366                 + ":" + this.offsetColor[1]
367                 + ":" + this.offsetColor[2]
368                 + ":" + this.quality[0]
369                 + ":" + ((null == this.grayscaleFilter) ? "color" : "grayscale")
370                 + ":" + super.getKey();
371     }
372     
373     public void recycle(){
374         super.recycle();
375         this.colorFilter = null;
376         this.grayscaleFilter = null;
377     }
378 }
379
Popular Tags