KickJava   Java API By Example, From Geeks To Geeks.

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


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 JavaDoc;
19 import java.awt.Font JavaDoc;
20 import java.awt.FontMetrics JavaDoc;
21 import java.awt.Graphics2D JavaDoc;
22 import java.awt.Image JavaDoc;
23 import java.awt.RenderingHints JavaDoc;
24 import java.awt.geom.Rectangle2D JavaDoc;
25 import java.awt.image.BufferedImage JavaDoc;
26 import java.awt.image.WritableRaster JavaDoc;
27 import java.io.ByteArrayOutputStream JavaDoc;
28 import java.io.IOException JavaDoc;
29 import java.lang.reflect.Field JavaDoc;
30 import java.util.Random JavaDoc;
31
32 import com.sun.image.codec.jpeg.JPEGCodec;
33 import com.sun.image.codec.jpeg.JPEGEncodeParam;
34 import com.sun.image.codec.jpeg.JPEGImageEncoder;
35
36 /**
37  * <p>The {@link CaptchaReader} is a simple tool generating JPEG images for the text
38  * supplied as its source in a way so that it's hard to parse automatically.</p>
39  *
40  * <p><i>CAPTCHA</i> means quite literally <i>Completely Automated Public Turing
41  * Test to Tell Computers and Humans Apart</i> and one of the best resources on
42  * this can be found at the <a HREF="http://www.captcha.net/">Carnegie Mellon
43  * School of Computer Science CAPTCHA project.</a>.
44  *
45  * <p>This reader creates very simple <i>CAPTCHAs</i> from within a Cocoon pipeline,
46  * enabling quick and safe end-user presence identificat. As an example, look at the
47  * following pipeline snippet:</p>
48  *
49  * <pre>
50  * &lt;map:match pattern="*"&gt;
51  * &lt;map:read type="captcha" SRC="{1}"/&gt;
52  * &lt;/map:match&gt;
53  * </pre>
54  *
55  * <p>The example will produce an image containing the text in <code>{1}</code>
56  * "warped" or "bent" in a way similar to the Adobe&reg; Photoshop&reg; "Wave"
57  * filter plugin.</p>
58  *
59  * <p>Few pipeline parameters control the operation of the {@link CaptchaReader}
60  * (this component is not configurable):</p>
61  *
62  * <ul>
63  * <li><code>width</code>: the width of the image to generate (default: 100).</li>
64  * <li><code>height</code>: the height of the image to generate (default: 50).</li>
65  * <li><code>foreground</code>: the text foreground color (default: random).</li>
66  * <li><code>background</code>: the image background color (default: white).</li>
67  * <li><code>font</code>: the font to use for the text (default: serif).</li>
68  * <li><code>scale</code>: the scaling factor for interim images (default: 5).</li>
69  * <li><code>amount</code>: the amount of text warping to apply (default: 1).</li>
70  * <li><code>quality</code>: the JPEG encoding quality (default: 0.75).</li>
71  * </ul>
72  *
73  * <p>Note that when the <code>foreground</code> parameter is not specified, the
74  * color used to write the text will be randomly chosen in a way that it contrasts
75  * well with the background color to avoid problems of illegible text.</p>
76  *
77  * <p>Both the <code>foreground</code> and <code>background</code> parameters accept
78  * strings in the format specified by {@link Color#decode(String)} (for example
79  * <code>fff</code>, or <code>0099CC</code>) or one of the field names of the
80  * {@link Color} class (for example {@link Color#BLACK BLACK} or {@link Color#cyan
81  * cyan} ...).</p>
82  *
83  * <p>The <code>scale</code> parameter controls how much the specified size should
84  * be scaled while processing the interim images: the bigger the scaling factor, the
85  * better the image quality, but also the memory used while generating the final
86  * image will be bigger. In other words, use with care.</p>
87  *
88  * <p>The <code>amount</code> parameter is interpreted as a floating point number
89  * and must be greater than zero. This controls how much text should be warped, and
90  * normally a value of <code>1</code> produce quite-good warping. Increasing (or
91  * decreasing) this value will produce more (ore less) warping.</p>
92  *
93  * <p>Remember that in no way the {@link CaptchaReader} claims to be able to
94  * generate "unbreakable" text (that will be impossible), and improvements to the
95  * algorithm are welcome.</p>
96  *
97  * @author <a HREF="mailto:pier@betaversion.org">Pier Fumagalli</a>
98  */

99 public class CaptchaReader extends AbstractReader {
100     
101     /** <p>A unique {@link Random} instance to use.</p> */
102     private static final Random JavaDoc RANDOM = new Random JavaDoc();
103
104     /**
105      * <p>The content type of the generated content: <code>image/jpeg</code>.</p>
106      *
107      * @return Always <code>image/jpeg</code>.
108      */

109     public String JavaDoc getMimeType() {
110         return "image/jpeg";
111     }
112
113     /**
114      * <p>Return a {@link Color} instance from a specified parameter.</p>
115      *
116      * @param parameterName The name of the parameter whose to use as the color.
117      * @param defaultColor The default {@link Color} to return.
118      * @return the interpreted color or the default color specified.
119      */

120     private Color JavaDoc getColor(String JavaDoc parameterName, Color JavaDoc defaultColor) {
121         String JavaDoc colorString = this.parameters.getParameter(parameterName, null);
122         if (colorString == null) return defaultColor;
123         try {
124             return Color.decode(colorString);
125         } catch (Exception JavaDoc e1) {
126             try {
127                 Field JavaDoc colorField = Color JavaDoc.class.getDeclaredField(colorString);
128                 return (Color JavaDoc) colorField.get(Color JavaDoc.class);
129             } catch (Exception JavaDoc e2) {
130                 return defaultColor;
131             }
132         }
133     }
134     
135     private Graphics2D JavaDoc antialiasedGraphics(BufferedImage JavaDoc image) {
136         Graphics2D JavaDoc graphics = image.createGraphics();
137         graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
138                                   RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
139         return graphics;
140     }
141
142     /**
143      * <p>Create an image containing the text specified as this reader source
144      * warped to avoid automatic interpretation.</p>
145      *
146      * @throws IOException if an I/O error occurred generating the image.
147      */

148     public void generate()
149     throws IOException JavaDoc {
150
151         /* Retrieve the current operational parameters from Cocoon's sitemap */
152         final int width = this.parameters.getParameterAsInteger("width", 100);
153         final int height = this.parameters.getParameterAsInteger("height", 50);
154         Color JavaDoc background = this.getColor("background", Color.white);
155         Color JavaDoc foreground = this.getColor("foreground", null);
156         if (foreground == null) {
157             int r = (RANDOM.nextInt(64) + 96 + background.getRed()) & 0x0ff;
158             int g = (RANDOM.nextInt(64) + 96 + background.getGreen()) & 0x0ff;
159             int b = (RANDOM.nextInt(64) + 96 + background.getBlue()) & 0x0ff;
160             foreground = new Color JavaDoc(r, g, b);
161         }
162         final String JavaDoc fontName = this.parameters.getParameter("font", "serif");
163         final int scale = this.parameters.getParameterAsInteger("scale", 5);
164         final float amount = this.parameters.getParameterAsFloat("amount", 2);
165         final float quality = this.parameters.getParameterAsFloat("quality", 0.75F);
166         final String JavaDoc text = this.source;
167
168         /* Create the final buffered image we will be writing to at the bottom */
169         final BufferedImage JavaDoc result = new BufferedImage JavaDoc(width, height,
170                                                        BufferedImage.TYPE_INT_RGB);
171
172         /* Starting with a size of 100, evaluate how big the real font should be */
173         final Font JavaDoc baseFont = new Font JavaDoc(fontName, Font.PLAIN, 100);
174         final Graphics2D JavaDoc graphics = this.antialiasedGraphics(result);
175         final FontMetrics JavaDoc metrics = graphics.getFontMetrics(baseFont);
176         final Rectangle2D JavaDoc tempSize = metrics.getStringBounds(text, graphics);
177
178         /* Evaluate the image size of the resulting image and prepare a ratio */
179         final double tempWidth = tempSize.getWidth() + (2 * tempSize.getHeight());
180         final double tempHeight = (tempSize.getHeight() * (1 + amount));
181         final double ratioWidth = width * scale / tempWidth;
182         final double ratioHeight = height * scale / tempHeight;
183         final double ratio = ratioWidth < ratioHeight? ratioWidth: ratioHeight;
184         final Font JavaDoc font = baseFont.deriveFont((float) (100 * ratio));
185
186         /* Evaluate the final size of the text to write */
187         final FontMetrics JavaDoc sourceMetrics = graphics.getFontMetrics(font);
188         final Rectangle2D JavaDoc size = sourceMetrics.getStringBounds(text, graphics);
189         final double textWidth = size.getWidth();
190         final double textHeight = size.getHeight();
191
192         /* Evaluate the final size of the interim images */
193         int scaledWidth = (int) (tempWidth * ratio);
194         int scaledHeight = (int) (tempHeight * ratio);
195         
196         /* Create a couple of images to write the plain string and the warped one */
197         BufferedImage JavaDoc source = new BufferedImage JavaDoc(scaledWidth, scaledHeight,
198                                                  BufferedImage.TYPE_BYTE_GRAY);
199         BufferedImage JavaDoc warped = new BufferedImage JavaDoc(scaledWidth, scaledHeight,
200                                                  BufferedImage.TYPE_INT_ARGB);
201
202         /* Prepare the background and the font of the source image */
203         final Graphics2D JavaDoc sourceGraphics = this.antialiasedGraphics(source);
204         sourceGraphics.setColor(Color.black);
205         sourceGraphics.fillRect(0, 0, scaledWidth, scaledHeight);
206         sourceGraphics.setFont(font);
207
208         /* Write the string exactly in the middle of the source image */
209         float textX = (float) ((scaledWidth - textWidth) / 2);
210         float textY = (float) ((scaledHeight - textHeight) / 2);
211         sourceGraphics.setColor(Color.white);
212         sourceGraphics.drawString(text, textX, textY + sourceMetrics.getAscent());
213
214         /* Randomize displacement factors for sine-waves */
215         final int displaceTop = RANDOM.nextInt(scaledWidth);
216         final int displaceBtm = RANDOM.nextInt(scaledWidth);
217         final int displaceVer = RANDOM.nextInt(scaledHeight);
218
219         /* Calculate the horizontal and vertical amplitude and wavelength of sines */
220         final double amplitHor = textHeight * amount / 4;
221         final double amplitVer = textHeight / 8;
222         final double t = (RANDOM.nextDouble() * textWidth / 2) + (textWidth * 0.75);
223         final double b = (RANDOM.nextDouble() * textWidth / 2) + (textWidth * 0.75);
224         final double wlenTop = textHeight > t? textHeight: t;
225         final double wlenBtm = textHeight > b? textHeight: b;
226
227         /* Calculate the offsets for horizontal (top and bottom) sine waves */
228         final double offsetTop = amplitHor;
229         final double offsetBtm = scaledHeight - amplitHor;
230
231         /* Prepare an array for vertical displacement sine wave */
232         final double vert[] = new double[scaledHeight];
233         for (int v = 0; v < scaledHeight ; v++) {
234             vert[v] = Math.sin((Math.PI * (v + displaceVer)) / textHeight) * amplitVer;
235         }
236
237         /* Iterate all the target image pixels and render the distortion */
238         int x1 = Integer.MAX_VALUE;
239         int x2 = Integer.MIN_VALUE;
240         int y1 = Integer.MAX_VALUE;
241         int y2 = Integer.MIN_VALUE;
242         final WritableRaster JavaDoc sourceRaster = source.getRaster();
243         final WritableRaster JavaDoc warpedRaster = warped.getRaster();
244         final double src[] = new double[9];
245         final double col[] = new double[] { foreground.getRed(),
246                                             foreground.getGreen(),
247                                             foreground.getBlue(), 0};
248         for (int h = 0; h < scaledWidth; h++) {
249             final double baseTop = (Math.PI * (h + displaceTop)) / wlenTop;
250             final double baseBtm = (Math.PI * (h + displaceBtm)) / wlenBtm;
251             final double top = offsetTop + Math.sin(baseTop) * amplitHor;
252             final double btm = offsetBtm - Math.sin(baseBtm) * amplitHor;
253
254             for (int v = 0; v < scaledHeight; v ++) {
255                 final double x = (h + vert[v]);
256                 final double y = (v * ((btm - top) / scaledHeight)) + top;
257
258                 if ((y > 0) && (y < scaledHeight - 1) &&
259                     (x > 0) && (x < scaledWidth - 1)) {
260
261                     /* Retrieve the nine pixels around the source one */
262                     sourceRaster.getPixels((int)(x-1), (int)(y-1), 3, 3, src);
263
264                     /* Average their value (it's grayscale) to have a better warp */
265                     double alpha = ((src[1] + src[3] + src[5] + src[7]) * 0.1) +
266                                    ((src[0] + src[2] + src[6] + src[8]) * 0.025) +
267                                    (src[4] * 0.5);
268
269                     /* Write the resultin pixel in the target image if necessary */
270                     if (alpha > 0) {
271                         col[3] = alpha;
272                         warpedRaster.setPixel(h, v, col);
273                         if (h < x1) x1 = h;
274                         if (h > x2) x2 = h;
275                         if (v < y1) y1 = v;
276                         if (v > y2) y2 = v;
277                     }
278                 }
279             }
280         }
281
282         /* Crop the image to the maximum extent of the warped text (if visible) */
283         source = null;
284         int xd = x2 - x1 + 1;
285         int yd = y2 - y1 + 1;
286         if ((xd > 1) && (yd > 1)) {
287             warped = warped.getSubimage(x1, y1, xd, yd);
288         }
289
290         /* Rescale the cropped image to the required size */
291         Image JavaDoc image = warped.getScaledInstance(width, height, Image.SCALE_SMOOTH);
292         graphics.setBackground(background);
293         graphics.setColor(background);
294         graphics.fillRect(0, 0, width, height);
295         graphics.setColor(foreground);
296         graphics.drawImage(image, 0, 0, null);
297         warped = null;
298
299         /* Write the processed image as a JPEG image */
300         ByteArrayOutputStream JavaDoc buffer = new ByteArrayOutputStream JavaDoc();
301         JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(buffer);
302         JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(result);
303         param.setQuality(quality, true);
304         encoder.encode(result, param);
305         buffer.flush();
306         buffer.close();
307         this.out.write(buffer.toByteArray());
308         this.out.flush();
309     }
310 }
311
Popular Tags