001: /* JHLabsCaptchaEngine.java
002:
003: {{IS_NOTE
004: Purpose:
005:
006: Description:
007:
008: History:
009: Tue Aug 1 10:30:48 2006, Created by henrichen
010: }}IS_NOTE
011:
012: Copyright (C) 2006 Potix Corporation. All Rights Reserved.
013:
014: {{IS_RIGHT
015: This program is distributed under GPL Version 2.0 in the hope that
016: it will be useful, but WITHOUT ANY WARRANTY.
017: }}IS_RIGHT
018: */
019: package org.zkoss.zkex.zul.impl;
020:
021: import org.zkoss.zul.*;
022: import org.zkoss.zul.impl.CaptchaEngine;
023: import org.zkoss.zk.ui.Component;
024: import org.zkoss.zk.ui.UiException;
025: import org.zkoss.image.AImage;
026: import org.zkoss.util.TimeZones;
027: import org.zkoss.lang.Strings;
028: import org.zkoss.lang.Objects;
029:
030: import com.jhlabs.image.RippleFilter;
031: import com.jhlabs.image.ShadowFilter;
032:
033: import java.awt.Font;
034: import java.awt.Color;
035: import java.awt.Image;
036: import java.awt.Toolkit;
037: import java.awt.Graphics2D;
038: import java.awt.BasicStroke;
039: import java.awt.GradientPaint;
040: import java.awt.RenderingHints;
041: import java.awt.image.ImageFilter;
042: import java.awt.image.ImageProducer;
043: import java.awt.image.BufferedImage;
044: import java.awt.image.FilteredImageSource;
045: import java.awt.geom.Point2D;
046: import java.awt.geom.Rectangle2D;
047: import java.awt.geom.CubicCurve2D;
048: import java.awt.geom.PathIterator;
049: import java.awt.geom.AffineTransform;
050: import java.awt.font.FontRenderContext;
051:
052: import java.util.Random;
053: import java.util.List;
054: import java.util.ArrayList;
055: import java.io.ByteArrayOutputStream;
056:
057: import javax.imageio.ImageIO;
058:
059: /**
060: * A captcha engine implemented with JH Labs libraries.
061: *
062: * <p>See also <a href="http://www.jhlabs.com/">JH Labs</a>
063: * @author henrichen
064: * @since 3.0.0
065: */
066: public class JHLabsCaptchaEngine implements CaptchaEngine,
067: java.io.Serializable {
068: private static Random _random = new Random();
069: private static final double[] sin = { //sin degree 0 ~ 10
070: Math.sin(Math.toRadians(0)), Math.sin(Math.toRadians(1)),
071: Math.sin(Math.toRadians(2)), Math.sin(Math.toRadians(3)),
072: Math.sin(Math.toRadians(4)), Math.sin(Math.toRadians(5)),
073: /* Math.sin(Math.toRadians(6)),
074: Math.sin(Math.toRadians(7)),
075: Math.sin(Math.toRadians(8)),
076: Math.sin(Math.toRadians(9)),
077: Math.sin(Math.toRadians(10)),
078: */};
079: private static final double[] cos = { //cos degree 0 ~ 10
080: Math.cos(Math.toRadians(0)), Math.cos(Math.toRadians(1)),
081: Math.cos(Math.toRadians(2)), Math.cos(Math.toRadians(3)),
082: Math.cos(Math.toRadians(4)), Math.cos(Math.toRadians(5)),
083: /* Math.cos(Math.toRadians(6)),
084: Math.cos(Math.toRadians(7)),
085: Math.cos(Math.toRadians(8)),
086: Math.cos(Math.toRadians(9)),
087: Math.cos(Math.toRadians(10)),
088: */};
089:
090: //-- CaptchaEngine --//
091: public byte[] generateCaptcha(Object data) {
092: final Captcha captcha = (Captcha) data;
093: final BufferedImage bi = drawCaptcha(captcha);
094:
095: //encode the image to jpeg format
096: final ByteArrayOutputStream baos = new ByteArrayOutputStream();
097: try {
098: if (!ImageIO.write(bi, "png", baos))
099: throw new UiException("Don't know how to generate PNG");
100: } catch (java.io.IOException ex) {
101: throw UiException.Aide.wrap(ex);
102: } finally {
103: try {
104: baos.close();
105: } catch (java.io.IOException ex) {
106: throw UiException.Aide.wrap(ex);
107: }
108: }
109: return baos.toByteArray();
110: }
111:
112: private BufferedImage drawCaptcha(Captcha captcha) {
113: final int width = captcha.getIntWidth();
114: final int height = captcha.getIntHeight();
115: final int bgRGB = captcha.getBgRGB();
116: final int fontRGB = captcha.getFontRGB();
117:
118: BufferedImage bi = new BufferedImage(width, height,
119: BufferedImage.TYPE_INT_ARGB);
120: BufferedImage bitgt = new BufferedImage(width, height,
121: BufferedImage.TYPE_INT_ARGB);
122: Graphics2D g2D = null;
123: Graphics2D gtgt = null;
124: try {
125: g2D = bi.createGraphics();
126:
127: //draw text
128: final Color fontColor = new Color(fontRGB);
129: g2D.setColor(fontColor);
130: int textwidth = drawText(g2D, captcha);
131:
132: //distortion
133: bi = distortion(bi);
134:
135: //box background
136: final Color bgColor = new Color(bgRGB, false);
137: GradientPaint grbgColor = new GradientPaint(0, 0, bgColor,
138: width, height, Color.WHITE);
139: gtgt = bitgt.createGraphics();
140: gtgt.setPaint(grbgColor);
141: gtgt.fillRect(0, 0, width, height);
142:
143: int wgap = width - textwidth;
144: int left = wgap <= 0 ? 0 : _random.nextInt(wgap);
145:
146: //draw the distortion image on the box
147: gtgt.drawImage(bi, left, 0, null);
148:
149: //draw the noise
150: if (captcha.isNoise()) {
151: genNoise(bitgt, .1f, .1f, .25f, .25f, fontColor);
152: genNoise(bitgt, .1f, .25f, .5f, .9f, fontColor);
153: }
154:
155: //box border
156: gtgt.setColor(fontColor);
157: gtgt.drawRect(0, 0, width - 1, height - 1);
158:
159: return bitgt;
160: } finally {
161: if (g2D != null)
162: g2D.dispose();
163: if (gtgt != null)
164: gtgt.dispose();
165: }
166: }
167:
168: protected int drawText(Graphics2D g2D, Captcha captcha) {
169: final int width = captcha.getIntWidth();
170: final int height = captcha.getIntHeight();
171: final String text = captcha.getValue();
172: final int len = text.length();
173: final int fontSize = captcha.getFonts().isEmpty() ? captcha
174: .getDefaultFonts().length : captcha.getFonts().size();
175: final int bgRGB = captcha.getBgRGB();
176: final int fontRGB = captcha.getFontRGB();
177: final Color bgColor = new Color(bgRGB, true);
178: final Color fontColor = new Color(fontRGB);
179:
180: int avgwidth = width / len;
181: int left = 0;
182: int top = 0;
183: int werror = 0; //adjust width error, so all words can be show correctly
184: for (int j = 0; j < len; ++j) {
185: final Font font = captcha
186: .getFont(_random.nextInt(fontSize));
187: g2D.setFont(font);
188: final String substr = text.substring(j, j + 1);
189: final FontRenderContext frc = g2D.getFontRenderContext();
190: final Rectangle2D r2D = font.getStringBounds(substr, frc);
191: BufferedImage charImg = rotateChar(substr, (int) r2D
192: .getWidth(), (int) r2D.getHeight(), font,
193: fontColor, bgColor);
194: final int charWidth = charImg.getWidth();
195: final int charHeight = charImg.getHeight();
196:
197: //gap to be move around
198: final int wgap = avgwidth - charWidth + werror;
199: if (wgap <= 0) {
200: werror = wgap;
201: }
202: final int hgap = height - charHeight;
203: left += (wgap <= 0 ? 0 : _random.nextInt(wgap));
204: top = (hgap <= 0 ? 0 : _random.nextInt(hgap));
205: g2D.drawImage(charImg, left, top, bgColor, null);
206: left += charWidth;
207: }
208: return left;
209: }
210:
211: protected BufferedImage rotateChar(String substr, int width,
212: int height, Font font, Color fontColor, Color bgColor) {
213: final int angle = _random.nextInt(11) - 5; //-5 ~ 5 degree
214: final int[] resultxy = newXy(width, height, angle);
215: final int newWidth = resultxy[0];
216: final int newHeight = resultxy[1];
217:
218: Graphics2D g2D = null;
219: try {
220: BufferedImage bi = new BufferedImage(newWidth, newHeight,
221: BufferedImage.TYPE_INT_ARGB);
222: g2D = bi.createGraphics();
223: RenderingHints hints = new RenderingHints(
224: RenderingHints.KEY_ANTIALIASING,
225: RenderingHints.VALUE_ANTIALIAS_ON);
226: hints.add(new RenderingHints(RenderingHints.KEY_RENDERING,
227: RenderingHints.VALUE_RENDER_QUALITY));
228: g2D.setRenderingHints(hints);
229:
230: //background
231: g2D.setColor(bgColor);
232: g2D.fillRect(0, 0, newWidth, newHeight);
233:
234: //draw text with rotate transformation
235: g2D.setFont(font);
236: int xRot = newWidth / 2;
237: int yRot = newHeight / 2;
238: AffineTransform xform = g2D.getTransform();
239: xform.rotate(Math.toRadians(angle), xRot, yRot);
240: g2D.setTransform(xform);
241:
242: g2D.setColor(fontColor);
243: int left = (newWidth - width) / 2;
244: int top = (newHeight - height) / 2 + (height * 2 / 3);
245: g2D.drawString(substr, left, top);
246:
247: return bi;
248: } finally {
249: if (g2D != null) {
250: g2D.dispose();
251: }
252: }
253: }
254:
255: private BufferedImage distortion(BufferedImage image) {
256: final int width = image.getWidth();
257: final int height = image.getHeight();
258:
259: //RippleFilter
260: RippleFilter rfilter = new RippleFilter();
261: rfilter.setWaveType(RippleFilter.SINE); //SINE or NOISE
262: rfilter.setXWavelength(_random.nextInt(8) + 9);
263: rfilter.setYWavelength(_random.nextInt(3) + 2);
264: rfilter.setXAmplitude(5.6f);
265: rfilter.setYAmplitude(_random.nextFloat() + 1.0f);
266:
267: image = rfilter.filter(image, null);
268:
269: //ShadowFilter
270: ShadowFilter sfilter = new ShadowFilter();
271: sfilter.setRadius(height / 4);
272: image = sfilter.filter(image, null);
273:
274: return image;
275: }
276:
277: private void genNoise(BufferedImage image, float factorOne,
278: float factorTwo, float factorThree, float factorFour,
279: Color fontColor) {
280: final int width = image.getWidth();
281: final int height = image.getHeight();
282:
283: //the curve from where the points are taken
284: CubicCurve2D cc = new CubicCurve2D.Float(width * factorOne
285: * _random.nextFloat(), height * _random.nextFloat(),
286: width * factorTwo, height * _random.nextFloat(), width
287: * factorThree, height * _random.nextFloat(),
288: width * factorFour, height * _random.nextFloat());
289:
290: // creates an iterator to define the boundary of the flattened curve
291: PathIterator pathIt = cc.getPathIterator(null, 2);
292: List points = new ArrayList(256);
293:
294: // while pathIt is iterating the curve, remember the points.
295: for (; !pathIt.isDone(); pathIt.next()) {
296: float[] coords = new float[6];
297: switch (pathIt.currentSegment(coords)) {
298: case PathIterator.SEG_MOVETO:
299: case PathIterator.SEG_LINETO:
300: points.add(new Point2D.Float(coords[0], coords[1]));
301: }
302: }
303:
304: Graphics2D g2D = null;
305: try {
306: g2D = (Graphics2D) image.getGraphics();
307: g2D.setRenderingHints(new RenderingHints(
308: RenderingHints.KEY_ANTIALIASING,
309: RenderingHints.VALUE_ANTIALIAS_ON));
310:
311: g2D.setColor(fontColor);
312:
313: final int count = points.size() - 1;
314: for (int j = 0; j < count; ++j) {
315: //for the first 3 point change the stroke and direction
316: if (j < 3) {
317: g2D.setStroke(new BasicStroke(0.9f * (3 - j)));
318: }
319: Point2D this Point = (Point2D) points.get(j);
320: Point2D nextPoint = (Point2D) points.get(j + 1);
321: g2D.drawLine((int) this Point.getX(), (int) this Point
322: .getY(), (int) nextPoint.getX(),
323: (int) nextPoint.getY());
324: }
325: } finally {
326: if (g2D != null) {
327: g2D.dispose();
328: }
329: }
330: }
331:
332: /** Given original x and y, and rotating degree, return the new x and y after rotation. */
333: private int[] newXy(int x, int y, int degree) {
334: //x' = s . cos(-org + degree), x' = x . cos(degree) + y . sin(degree)
335: //y' = s . sin( org + degree), y' = y . cos(degree) + x . sin(degree)
336: //x' = s . cos(-org - degree) = s . cos(org + degree), x' = x . cos(degree) - y . sin(degree)
337: //y' = s . sin( org - degree), y' = y . cos(degree) + x . sin(degree)
338: if (degree < 0) {
339: degree = -degree;
340: x = (int) (x * cos[degree] + y * sin[degree]);
341: y = (int) (y * cos[degree] - x * sin[degree]);
342: } else {
343: x = (int) (x * cos[degree] - y * sin[degree]);
344: y = (int) (y * cos[degree] + x * sin[degree]);
345: }
346: final int[] result = new int[2];
347: result[0] = x;
348: result[1] = y;
349:
350: return result;
351: }
352: }
|