SRL Archive - Java Help and Tutorials

Captcha generator


weequ

2012-06-23 17:02:17

Edit: Scroll down for more recent version.


Do you think it's good enough or is there existing solvers that would solve that?

Features:
*Randomly move letters
*Randomly rotate letters
*Randomly shear letters
*Paint outline of letters
*Add randomly generated curve to connect letters
*Randomly add black and white lines and curves to letters.

code:

package captchagenerator;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
*
* @author Antti
*/
public class CaptchaGenerator extends JFrame{

/**
* @param args the command line arguments
*/

private BufferedImage img;
JPanel panel;


public CaptchaGenerator() {
setTitle("Captcha Generator");
//panel = new JPanel(new FlowLayout());
setLayout(new FlowLayout());
img = generateCaptcha("Captcha testing");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setVisible(true);
JButton jbutton = new JButton("Generate Captcha");
add(jbutton);
pack();
setSize(500, 500);
jbutton.addActionListener(new ActionListener() {

@Override
public void actionPerformed(ActionEvent e) {
img = generateCaptcha("Captcha testing");
repaint();
}
});
}

@Override
public void paint(Graphics g) {
super.paint(g);
g.drawImage(img, 50, 100, null);
}

// public static BufferedImage generateLetter(char c, float angle) {
// Font f = new Font("arial", Font.BOLD, 30);
// FontRenderContext fc = new FontRenderContext(f.getTransform(), true, false);
// Rectangle2D stringBounds = f.getStringBounds(""+c, fc);
// BufferedImage img2 = new BufferedImage((int)stringBounds.getWidth(), (int)stringBounds.getHeight(), BufferedImage.TYPE_INT_RGB);
//
// return null;
// }

public static BufferedImage generateCaptcha(String text) {
Font f = new Font("arial", Font.PLAIN, 30);
FontRenderContext at = new FontRenderContext(f.getTransform(), true, false);

GlyphVector gv = f.createGlyphVector(at, text);

//Rotating letters
double currentAngle = 0;
for (int i = 0; i < gv.getNumGlyphs(); i++) {
Point2D glyphPosition = gv.getGlyphPosition(i);
glyphPosition.setLocation(glyphPosition.getX()-2+Math.random()*4, glyphPosition.getY()-2+Math.random()*4);
gv.setGlyphPosition(i, glyphPosition);
currentAngle+=Math.random()*4-2;
gv.setGlyphTransform(i, AffineTransform.getRotateInstance(Math.toRadians(currentAngle), gv.getVisualBounds().getMaxX()*0.5, gv.getVisualBounds().getMaxY()*0.5));
}

BufferedImage img2 = new BufferedImage((int)gv.getVisualBounds().getWidth()+1, (int)gv.getVisualBounds().getHeight()+1, BufferedImage.TYPE_INT_RGB);
Graphics2D g = img2.createGraphics();
Shape s = gv.getOutline();

g.translate(-gv.getVisualBounds().getMinX(), -gv.getVisualBounds().getMinY());
g.setColor(Color.WHITE);
g.draw(s);


//Adding lines and curves to letters
int next = Math.round(Math.round((Math.random()*13)));
PathIterator pi = gv.getOutline().getPathIterator(null);
double[] coordinates = new double[6];
while (!pi.isDone()) {
int seg = pi.currentSegment(coordinates);
switch (seg) {
case PathIterator.SEG_CLOSE:
next--;
break;
case PathIterator.SEG_MOVETO:
next--;
break;
case PathIterator.SEG_LINETO:
next--;
break;
case PathIterator.SEG_QUADTO:
next -= 2;
break;
case PathIterator.SEG_CUBICTO:
next -= 3;
break;
}

if (next <= 0) {
int r = Math.round(Math.round(Math.random()*-next));
next = 4+(int)(Math.random()*8);
if (Math.random() < 0.5) {
g.setColor(Color.WHITE);
} else {
g.setColor(Color.BLACK);
}
switch (Math.round(Math.round(Math.random()*3))) {
case 1:
g.drawLine(Math.round(Math.round(coordinates[r])), Math.round(Math.round(coordinates[r+1])), Math.round(Math.round(coordinates[r]))-7+Math.round(Math.round(Math.random()*14)), Math.round(Math.round(coordinates[r+1]))-7+Math.round(Math.round(Math.random()*14)));
break;
case 2:
QuadCurve2D.Double qc = new QuadCurve2D.Double(coordinates[r], coordinates[r+1],
Math.round(Math.round(coordinates[r]))-7+Math.round(Math.round(Math.random()*14)), Math.round(Math.round(coordinates[r+1]))-7+Math.round(Math.round(Math.random()*14)),
Math.round(Math.round(coordinates[r]))-7+Math.round(Math.round(Math.random()*14)), Math.round(Math.round(coordinates[r+1]))-7+Math.round(Math.round(Math.random()*14)));
g.draw(qc);
break;
case 3:
CubicCurve2D cc = new CubicCurve2D.Double(coordinates[r], coordinates[r+1],
Math.round(Math.round(coordinates[r]))-7+Math.round(Math.round(Math.random()*14)), Math.round(Math.round(coordinates[r+1]))-7+Math.round(Math.round(Math.random()*14)),
Math.round(Math.round(coordinates[r]))-7+Math.round(Math.round(Math.random()*14)), Math.round(Math.round(coordinates[r+1]))-7+Math.round(Math.round(Math.random()*14)),
Math.round(Math.round(coordinates[r]))-7+Math.round(Math.round(Math.random()*14)), Math.round(Math.round(coordinates[r+1]))-7+Math.round(Math.round(Math.random()*14)));
g.draw(cc);
break;

}
}
pi.next();
}

//g.translate(gv.getVisualBounds().getMinX(), gv.getVisualBounds().getMinY());

img2.flush();
return img2;
}


public static void main(String[] args) {
CaptchaGenerator cg = new CaptchaGenerator();
}
}


Brandon

2012-06-23 17:08:39

Existing Solvers WILL Solve that for sure. I used to have a solver from a BlackHat forum that extracted noise similar to that. You should add more noise for sure! That's too little. I think Simba could solve that if someone really made the fonts.

The rotating you have should mess up Captcha's though but in the image you posted, it will get solved. Post more images?

weequ

2012-06-23 17:18:50

From: Brandon
Existing Solvers WILL Solve that for sure. I used to have a solver from a BlackHat forum that extracted noise similar to that. You should add more noise for sure! That's too little. I think Simba could solve that if someone really made the fonts.

The rotating you have should mess up Captcha's though but in the image you posted, it will get solved. Post more images?
Thanks. I will add more noise (easy to do) and I guess I should also consider 3D rotation (not so easy).

Edit: more noise added. If anyone has solvers could they please try with that image?


weequ

2012-07-05 08:15:44

Been working on this quite much. Here is what it currently looks like:
Need your opinion. Better or worse than previously?

Any suggestions on how to improve?

Brandon

2012-07-05 10:12:30

WAY!!! Better! That's much harder to break. Especially the first one.

Home

2012-07-05 11:09:36

Can you make it use random font for each letter and random color of course.

~Home

weequ

2012-07-05 14:32:17

Thanks for your responses.

From: Home
Can you make it use random font for each letter and random color of course.

~Home
I could quite easily make it use random font for each letter but then it would be harder to read for humans too. idk.

Random color for each letter would be a bad idea I think because then it would be too easy to identify the letters from each other.

One thing I might consider is adding background and making the letters semi transparent. Any ideas on how to generate random background?


I updated a bit. (Fixed a bug with rotating as well as made the height of the image correct (the height of the tallest letter)).

Updated version:

Edit: src of the most recent version:


/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package captchagenerator;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
*
* @author Antti
*/
public class CaptchaGenerator extends JFrame{

/**
* @param args the command line arguments
*/

private BufferedImage img;
JPanel panel;


public CaptchaGenerator() {
setTitle("Captcha Generator");
//panel = new JPanel(new FlowLayout());
setLayout(new FlowLayout());
img = generateCaptcha("CaptchaTesting");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setVisible(true);
JButton jbutton = new JButton("Generate Captcha");
add(jbutton);
pack();
setSize(500, 500);
jbutton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
img = generateCaptcha(generatestring(5));
repaint();
}
});
}

@Override
public void paint(Graphics g) {
super.paint(g);
g.drawImage(img, 50, 100, null);
}

public String generatestring(int len) {
//String chars = "0123456789";//"123456789abcdefghkmnopqsuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
String chars = "123456789abdefghkmnpqsuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
char[] tulos = new char[len];
for (int i = 0; i < len; i++) {
tulos[i] = chars.charAt((int)(Math.random()*chars.length()));
}
return new String(tulos, 0, len);
}


public static double distance(Shape left, Shape right) {
return right.getBounds2D().getMinX()-left.getBounds2D().getMaxX();
}

public static BufferedImage generateCaptcha(String text) {
Font f = new Font("arial", Font.PLAIN, 40);
FontRenderContext at = new FontRenderContext(f.getTransform(), true, false);

GlyphVector gv = f.createGlyphVector(at, text);
//Repositioning, shearing and rotating letters
double maxHeight = 0;
for (int i = 0; i < gv.getNumGlyphs(); i++) {
double lastangle = Math.toRadians(Math.random()*80-40);
AffineTransform af = new AffineTransform();
af.setToRotation(lastangle);
af.shear(0.4-Math.random()*0.8, 0.4-Math.random()*0.8);
gv.setGlyphTransform(i, af);
Point2D glyphPosition = gv.getGlyphPosition(i);
if (i > 0) {
if (gv.getGlyphVisualBounds(i).getBounds2D().getHeight() > maxHeight) {
maxHeight = gv.getGlyphVisualBounds(i).getBounds2D().getHeight();
}
double dist = distance(gv.getGlyphVisualBounds(i-1), gv.getGlyphVisualBounds(i));
glyphPosition.setLocation(glyphPosition.getX()-dist-Math.random()*2, glyphPosition.getY()-2+Math.random()*4);
gv.setGlyphPosition(i, glyphPosition);
}
}

//Randomly set y positions of the characters limited by the tallest character.
for (int i = 0; i < gv.getNumGlyphs(); i++) {
gv.setGlyphPosition(i, new Point2D.Double(gv.getGlyphPosition(i).getX(), gv.getGlyphPosition(i).getY()-gv.getGlyphVisualBounds(i).getBounds2D().getY()+Math.random()*(maxHeight-gv.getGlyphVisualBounds(i).getBounds2D().getHeight())));
}

//Create 1-bit (black and white) bufferedimage with the correct size
BufferedImage img2 = new BufferedImage((int)gv.getVisualBounds().getWidth()+2, (int)gv.getVisualBounds().getHeight()+2, BufferedImage.TYPE_BYTE_BINARY);

Graphics2D g = img2.createGraphics();
BasicStroke bs = new BasicStroke(2.1f,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER);
g.setStroke(bs);
g.translate(-gv.getVisualBounds().getMinX()+1, -gv.getVisualBounds().getMinY()+1);//Not needed

//Creating a curved shape connecting letters
CubicCurve2D cc2d = new CubicCurve2D.Double(gv.getVisualBounds().getMinX()+Math.random()*5, gv.getVisualBounds().getMinY()+gv.getVisualBounds().getHeight()*Math.random()-1.5,
gv.getVisualBounds().getWidth()/3, gv.getVisualBounds().getCenterY()+gv.getVisualBounds().getHeight()*(Math.random()-0.5)-1.5,
gv.getVisualBounds().getWidth()/2, gv.getVisualBounds().getCenterY()+gv.getVisualBounds().getHeight()*(Math.random()-0.5)-1.5,
gv.getVisualBounds().getMaxX()-Math.random()*5, gv.getVisualBounds().getMinY()+gv.getVisualBounds().getHeight()*Math.random()-1.5);
GeneralPath gp = new GeneralPath();
gp.moveTo(cc2d.getX1(), cc2d.getY1());
gp.curveTo(cc2d.getCtrlX1(), cc2d.getCtrlY1(),
cc2d.getCtrlX2(), cc2d.getCtrlY2(),
cc2d.getX2(), cc2d.getY2());

gp.lineTo(cc2d.getX2(), cc2d.getY2()+3);
gp.curveTo(cc2d.getCtrlX2(), cc2d.getCtrlY2()+3,
cc2d.getCtrlX1(), cc2d.getCtrlY1()+3,
cc2d.getX1(), cc2d.getY1()+3);
gp.closePath();

//Draw the curved shape
g.setColor(Color.WHITE);
g.draw(gp);

//Draw letters
g.draw(gv.getOutline());
g.setColor(Color.BLACK);
g.fill(gv.getOutline());

//Fill curved shape
g.setColor(Color.BLACK);
g.fill(gp);

img2.flush();
return img2;
}


public static void main(String[] args) {
CaptchaGenerator cg = new CaptchaGenerator();
}
}