/* | |
* $Id: SmartLinkLabel.java 5860 2006-05-25 20:29:28 +0000 (Thu, 25 May 2006) | |
* eelco12 $ $Revision: 5876 $ $Date: 2006-05-25 20:29:28 +0000 (Thu, 25 May | |
* 2006) $ | |
* | |
* ============================================================================== | |
* Licensed under the Apache License, Version 2.0 (the "License"); you may not | |
* use this file except in compliance with the License. You may obtain a copy of | |
* the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
* License for the specific language governing permissions and limitations under | |
* the License. | |
*/ | |
package wicket.extensions.markup.html.captcha; | |
import java.awt.BasicStroke; | |
import java.awt.Color; | |
import java.awt.Font; | |
import java.awt.Graphics2D; | |
import java.awt.Shape; | |
import java.awt.font.FontRenderContext; | |
import java.awt.font.TextLayout; | |
import java.awt.geom.AffineTransform; | |
import java.awt.image.BufferedImage; | |
import java.awt.image.WritableRaster; | |
import java.io.Serializable; | |
import java.lang.ref.SoftReference; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.List; | |
import java.util.Random; | |
import wicket.markup.html.image.resource.DynamicImageResource; | |
import wicket.util.time.Time; | |
/** | |
* Generates a captcha image. | |
* | |
* @author Joshua Perlow | |
*/ | |
public final class CaptchaImageResource extends DynamicImageResource | |
{ | |
/** | |
* This class is used to encapsulate all the filters that a character will | |
* get when rendered. The changes are kept so that the size of the shapes | |
* can be properly recorded and reproduced later, since it dynamically | |
* generates the size of the captcha image. The reason I did it this way is | |
* because none of the JFC graphics classes are serializable, so they cannot | |
* be instance variables here. If anyone knows a better way to do this, | |
* please let me know. | |
*/ | |
private static final class CharAttributes implements Serializable | |
{ | |
private static final long serialVersionUID = 1L; | |
private char c; | |
private String name; | |
private int rise; | |
private double rotation; | |
private double shearX; | |
private double shearY; | |
CharAttributes(char c, String name, double rotation, int rise, double shearX, double shearY) | |
{ | |
this.c = c; | |
this.name = name; | |
this.rotation = rotation; | |
this.rise = rise; | |
this.shearX = shearX; | |
this.shearY = shearY; | |
} | |
char getChar() | |
{ | |
return c; | |
} | |
String getName() | |
{ | |
return name; | |
} | |
int getRise() | |
{ | |
return rise; | |
} | |
double getRotation() | |
{ | |
return rotation; | |
} | |
double getShearX() | |
{ | |
return shearX; | |
} | |
double getShearY() | |
{ | |
return shearY; | |
} | |
} | |
private static final long serialVersionUID = 1L; | |
private static int randomInt(int min, int max) | |
{ | |
return (int)(Math.random() * (max - min) + min); | |
} | |
private static String randomString(int min, int max) | |
{ | |
int num = randomInt(min, max); | |
byte b[] = new byte[num]; | |
for (int i = 0; i < num; i++) | |
b[i] = (byte)randomInt('a', 'z'); | |
return new String(b); | |
} | |
private String challengeId; | |
private final List charAttsList; | |
private List fontNames = Arrays.asList(new String[] { "Helventica", "Arial", "Courier" }); | |
private final int fontSize; | |
private final int fontStyle; | |
private int height = 0; | |
/** Transient image data so that image only needs to be generated once per VM */ | |
private transient SoftReference imageData; | |
private final int margin; | |
private int width = 0; | |
/** | |
* Construct. | |
*/ | |
public CaptchaImageResource() | |
{ | |
this(randomString(6, 8)); | |
} | |
/** | |
* Construct. | |
* | |
* @param challengeId | |
* The id of the challenge | |
*/ | |
public CaptchaImageResource(String challengeId) | |
{ | |
this(challengeId, 48, 30); | |
} | |
/** | |
* Construct. | |
* | |
* @param challengeId | |
* The id of the challenge | |
* @param fontSize | |
* The font size | |
* @param margin | |
* The image's margin | |
*/ | |
public CaptchaImageResource(String challengeId, int fontSize, int margin) | |
{ | |
this.challengeId = challengeId; | |
this.fontStyle = 1; | |
this.fontSize = fontSize; | |
this.margin = margin; | |
this.width = this.margin * 2; | |
this.height = this.margin * 2; | |
char[] chars = challengeId.toCharArray(); | |
charAttsList = new ArrayList(); | |
TextLayout text; | |
AffineTransform textAt; | |
Shape shape; | |
for (int i = 0; i < chars.length; i++) | |
{ | |
String fontName = (String)fontNames.get(randomInt(0, fontNames.size())); | |
double rotation = Math.toRadians(randomInt(-35, 35)); | |
int rise = randomInt(margin / 2, margin); | |
Random ran = new Random(); | |
double shearX = ran.nextDouble() * 0.2; | |
double shearY = ran.nextDouble() * 0.2; | |
CharAttributes cf = new CharAttributes(chars[i], fontName, rotation, rise, shearX, | |
shearY); | |
charAttsList.add(cf); | |
text = new TextLayout(chars[i] + "", getFont(fontName), new FontRenderContext(null, | |
false, false)); | |
textAt = new AffineTransform(); | |
textAt.rotate(rotation); | |
textAt.shear(shearX, shearY); | |
shape = text.getOutline(textAt); | |
this.width += (int)shape.getBounds2D().getWidth(); | |
if (this.height < (int)shape.getBounds2D().getHeight() + rise) | |
{ | |
this.height = (int)shape.getBounds2D().getHeight() + rise; | |
} | |
} | |
} | |
/** | |
* Gets the id for the challenge. | |
* | |
* @return The the id for the challenge | |
*/ | |
public final String getChallengeId() | |
{ | |
return challengeId; | |
} | |
/** | |
* Causes the image to be redrawn the next time its requested. | |
* | |
* @see wicket.Resource#invalidate() | |
*/ | |
public final void invalidate() | |
{ | |
imageData = null; | |
} | |
/** | |
* @see wicket.markup.html.image.resource.DynamicImageResource#getImageData() | |
*/ | |
protected final byte[] getImageData() | |
{ | |
// get image data is always called in sync block | |
byte[] data = null; | |
if (imageData != null) | |
{ | |
data = (byte[])imageData.get(); | |
} | |
if (data == null) | |
{ | |
data = render(); | |
imageData = new SoftReference(data); | |
setLastModifiedTime(Time.now()); | |
} | |
return data; | |
} | |
private Font getFont(String fontName) | |
{ | |
return new Font(fontName, fontStyle, fontSize); | |
} | |
/** | |
* Renders this image | |
* | |
* @return The image data | |
*/ | |
private final byte[] render() | |
{ | |
while (true) | |
{ | |
final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); | |
Graphics2D gfx = (Graphics2D)image.getGraphics(); | |
gfx.setBackground(Color.WHITE); | |
int curWidth = margin; | |
for (int i = 0; i < charAttsList.size(); i++) | |
{ | |
CharAttributes cf = (CharAttributes)charAttsList.get(i); | |
TextLayout text = new TextLayout(cf.getChar() + "", getFont(cf.getName()), gfx | |
.getFontRenderContext()); | |
AffineTransform textAt = new AffineTransform(); | |
textAt.translate(curWidth, height - cf.getRise()); | |
textAt.rotate(cf.getRotation()); | |
textAt.shear(cf.getShearX(), cf.getShearY()); | |
Shape shape = text.getOutline(textAt); | |
curWidth += shape.getBounds().getWidth(); | |
gfx.setXORMode(Color.BLACK); | |
gfx.fill(shape); | |
} | |
// XOR circle | |
int dx = randomInt(width, 2 * width); | |
int dy = randomInt(width, 2 * height); | |
int x = randomInt(0, width / 2); | |
int y = randomInt(0, height / 2); | |
gfx.setXORMode(Color.BLACK); | |
gfx.setStroke(new BasicStroke(randomInt(fontSize / 8, fontSize / 2))); | |
gfx.drawOval(x, y, dx, dy); | |
WritableRaster rstr = image.getRaster(); | |
int[] vColor = new int[3]; | |
int[] oldColor = new int[3]; | |
Random vRandom = new Random(System.currentTimeMillis()); | |
// noise | |
for (x = 0; x < width; x++) | |
{ | |
for (y = 0; y < height; y++) | |
{ | |
rstr.getPixel(x, y, oldColor); | |
// hard noise | |
vColor[0] = 0 + (int)(Math.floor(vRandom.nextFloat() * 1.03) * 255); | |
// soft noise | |
vColor[0] = vColor[0] ^ (170 + (int)(vRandom.nextFloat() * 80)); | |
// xor to image | |
vColor[0] = vColor[0] ^ oldColor[0]; | |
vColor[1] = vColor[0]; | |
vColor[2] = vColor[0]; | |
rstr.setPixel(x, y, vColor); | |
} | |
} | |
return toImageData(image); | |
} | |
} | |
} |