001: /*
002: * $Id: SmartLinkLabel.java 5860 2006-05-25 20:29:28 +0000 (Thu, 25 May 2006)
003: * eelco12 $ $Revision: 5876 $ $Date: 2006-05-25 20:29:28 +0000 (Thu, 25 May
004: * 2006) $
005: *
006: * ==============================================================================
007: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
008: * use this file except in compliance with the License. You may obtain a copy of
009: * the License at
010: *
011: * http://www.apache.org/licenses/LICENSE-2.0
012: *
013: * Unless required by applicable law or agreed to in writing, software
014: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
015: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
016: * License for the specific language governing permissions and limitations under
017: * the License.
018: */
019: package wicket.extensions.markup.html.captcha;
020:
021: import java.awt.BasicStroke;
022: import java.awt.Color;
023: import java.awt.Font;
024: import java.awt.Graphics2D;
025: import java.awt.Shape;
026: import java.awt.font.FontRenderContext;
027: import java.awt.font.TextLayout;
028: import java.awt.geom.AffineTransform;
029: import java.awt.image.BufferedImage;
030: import java.awt.image.WritableRaster;
031: import java.io.Serializable;
032: import java.lang.ref.SoftReference;
033: import java.util.ArrayList;
034: import java.util.Arrays;
035: import java.util.List;
036: import java.util.Random;
037:
038: import wicket.markup.html.image.resource.DynamicImageResource;
039: import wicket.util.time.Time;
040:
041: /**
042: * Generates a captcha image.
043: *
044: * @author Joshua Perlow
045: */
046: public final class CaptchaImageResource extends DynamicImageResource {
047: /**
048: * This class is used to encapsulate all the filters that a character will
049: * get when rendered. The changes are kept so that the size of the shapes
050: * can be properly recorded and reproduced later, since it dynamically
051: * generates the size of the captcha image. The reason I did it this way is
052: * because none of the JFC graphics classes are serializable, so they cannot
053: * be instance variables here. If anyone knows a better way to do this,
054: * please let me know.
055: */
056: private static final class CharAttributes implements Serializable {
057: private static final long serialVersionUID = 1L;
058: private char c;
059: private String name;
060: private int rise;
061: private double rotation;
062: private double shearX;
063: private double shearY;
064:
065: CharAttributes(char c, String name, double rotation, int rise,
066: double shearX, double shearY) {
067: this .c = c;
068: this .name = name;
069: this .rotation = rotation;
070: this .rise = rise;
071: this .shearX = shearX;
072: this .shearY = shearY;
073: }
074:
075: char getChar() {
076: return c;
077: }
078:
079: String getName() {
080: return name;
081: }
082:
083: int getRise() {
084: return rise;
085: }
086:
087: double getRotation() {
088: return rotation;
089: }
090:
091: double getShearX() {
092: return shearX;
093: }
094:
095: double getShearY() {
096: return shearY;
097: }
098: }
099:
100: private static final long serialVersionUID = 1L;
101:
102: private static int randomInt(int min, int max) {
103: return (int) (Math.random() * (max - min) + min);
104: }
105:
106: private static String randomString(int min, int max) {
107: int num = randomInt(min, max);
108: byte b[] = new byte[num];
109: for (int i = 0; i < num; i++)
110: b[i] = (byte) randomInt('a', 'z');
111: return new String(b);
112: }
113:
114: private String challengeId;
115: private final List charAttsList;
116:
117: private List fontNames = Arrays.asList(new String[] { "Helventica",
118: "Arial", "Courier" });
119: private final int fontSize;
120: private final int fontStyle;
121:
122: private int height = 0;
123:
124: /** Transient image data so that image only needs to be generated once per VM */
125: private transient SoftReference imageData;
126:
127: private final int margin;
128:
129: private int width = 0;
130:
131: /**
132: * Construct.
133: */
134: public CaptchaImageResource() {
135: this (randomString(6, 8));
136: }
137:
138: /**
139: * Construct.
140: *
141: * @param challengeId
142: * The id of the challenge
143: */
144: public CaptchaImageResource(String challengeId) {
145: this (challengeId, 48, 30);
146: }
147:
148: /**
149: * Construct.
150: *
151: * @param challengeId
152: * The id of the challenge
153: * @param fontSize
154: * The font size
155: * @param margin
156: * The image's margin
157: */
158: public CaptchaImageResource(String challengeId, int fontSize,
159: int margin) {
160: this .challengeId = challengeId;
161: this .fontStyle = 1;
162: this .fontSize = fontSize;
163: this .margin = margin;
164: this .width = this .margin * 2;
165: this .height = this .margin * 2;
166: char[] chars = challengeId.toCharArray();
167: charAttsList = new ArrayList();
168: TextLayout text;
169: AffineTransform textAt;
170: Shape shape;
171: for (int i = 0; i < chars.length; i++) {
172: String fontName = (String) fontNames.get(randomInt(0,
173: fontNames.size()));
174: double rotation = Math.toRadians(randomInt(-35, 35));
175: int rise = randomInt(margin / 2, margin);
176: Random ran = new Random();
177: double shearX = ran.nextDouble() * 0.2;
178: double shearY = ran.nextDouble() * 0.2;
179: CharAttributes cf = new CharAttributes(chars[i], fontName,
180: rotation, rise, shearX, shearY);
181: charAttsList.add(cf);
182: text = new TextLayout(chars[i] + "", getFont(fontName),
183: new FontRenderContext(null, false, false));
184: textAt = new AffineTransform();
185: textAt.rotate(rotation);
186: textAt.shear(shearX, shearY);
187: shape = text.getOutline(textAt);
188: this .width += (int) shape.getBounds2D().getWidth();
189: if (this .height < (int) shape.getBounds2D().getHeight()
190: + rise) {
191: this .height = (int) shape.getBounds2D().getHeight()
192: + rise;
193: }
194: }
195: }
196:
197: /**
198: * Gets the id for the challenge.
199: *
200: * @return The the id for the challenge
201: */
202: public final String getChallengeId() {
203: return challengeId;
204: }
205:
206: /**
207: * Causes the image to be redrawn the next time its requested.
208: *
209: * @see wicket.Resource#invalidate()
210: */
211: public final void invalidate() {
212: imageData = null;
213: }
214:
215: /**
216: * @see wicket.markup.html.image.resource.DynamicImageResource#getImageData()
217: */
218: protected final byte[] getImageData() {
219: // get image data is always called in sync block
220: byte[] data = null;
221: if (imageData != null) {
222: data = (byte[]) imageData.get();
223: }
224: if (data == null) {
225: data = render();
226: imageData = new SoftReference(data);
227: setLastModifiedTime(Time.now());
228: }
229: return data;
230: }
231:
232: private Font getFont(String fontName) {
233: return new Font(fontName, fontStyle, fontSize);
234: }
235:
236: /**
237: * Renders this image
238: *
239: * @return The image data
240: */
241: private final byte[] render() {
242: while (true) {
243: final BufferedImage image = new BufferedImage(width,
244: height, BufferedImage.TYPE_INT_RGB);
245: Graphics2D gfx = (Graphics2D) image.getGraphics();
246: gfx.setBackground(Color.WHITE);
247: int curWidth = margin;
248: for (int i = 0; i < charAttsList.size(); i++) {
249: CharAttributes cf = (CharAttributes) charAttsList
250: .get(i);
251: TextLayout text = new TextLayout(cf.getChar() + "",
252: getFont(cf.getName()), gfx
253: .getFontRenderContext());
254: AffineTransform textAt = new AffineTransform();
255: textAt.translate(curWidth, height - cf.getRise());
256: textAt.rotate(cf.getRotation());
257: textAt.shear(cf.getShearX(), cf.getShearY());
258: Shape shape = text.getOutline(textAt);
259: curWidth += shape.getBounds().getWidth();
260: gfx.setXORMode(Color.BLACK);
261: gfx.fill(shape);
262: }
263:
264: // XOR circle
265: int dx = randomInt(width, 2 * width);
266: int dy = randomInt(width, 2 * height);
267: int x = randomInt(0, width / 2);
268: int y = randomInt(0, height / 2);
269:
270: gfx.setXORMode(Color.BLACK);
271: gfx.setStroke(new BasicStroke(randomInt(fontSize / 8,
272: fontSize / 2)));
273: gfx.drawOval(x, y, dx, dy);
274:
275: WritableRaster rstr = image.getRaster();
276: int[] vColor = new int[3];
277: int[] oldColor = new int[3];
278: Random vRandom = new Random(System.currentTimeMillis());
279:
280: // noise
281: for (x = 0; x < width; x++) {
282: for (y = 0; y < height; y++) {
283: rstr.getPixel(x, y, oldColor);
284:
285: // hard noise
286: vColor[0] = 0 + (int) (Math.floor(vRandom
287: .nextFloat() * 1.03) * 255);
288: // soft noise
289: vColor[0] = vColor[0]
290: ^ (170 + (int) (vRandom.nextFloat() * 80));
291: // xor to image
292: vColor[0] = vColor[0] ^ oldColor[0];
293: vColor[1] = vColor[0];
294: vColor[2] = vColor[0];
295:
296: rstr.setPixel(x, y, vColor);
297: }
298: }
299: return toImageData(image);
300: }
301: }
302: }
|