Bwcaptcha: Another Implementation of Captcha

From Documentation
DocumentationSmall Talks2010JanuaryBwcaptcha: Another Implementation of Captcha
Bwcaptcha: Another Implementation of Captcha

Author
Jimmy Shiau, Engineer, Potix Corporation
Date
January 5, 2010
Version
Applicable to ZK 5.0 and later

Introduction

The bwcaptcha component was originally created by Dennis Chen. I have made it compatible with ZK 5 recently. The API is similar to the standard captcha component, such as specifying the color, size, font and so on. In this small talk, I'll show you how to use it and how it is implemented. You'll see how easy it is to create a BW captcha ZK component.

How to use the bwcaptcha component

Here is the bwcaptcha's attribute specification table:

Attribute Usage Default Value
width Sets the width of the captcha "135" (unit is pixel)
height Sets the height of the captcha "55" (unit is pixel)
bgColor Sets the background color of the captcha 0x8888FF (in 0xRRGGBB)
fontColor Sets the font color of the captcha 0xFF6666 (in 0xRRGGBB)
fontBgColor Sets the font background color of the captcha 0xFFDDFF (in 0xRRGGBB)
lineColor Sets the line color of the captcha 0x7777DD (in 0xRRGGBB)
thickness Set thickness of line 3
font Sets the font of the captcha new Font("Courier", Font.BOLD, 32)
length Sets length of the auto generated text value 5

Create the bwcaptcha component in your zul file

You can use a <bw.captcha/> tag to create a bwcaptcha component, and call the getValue method to retrieve the value of the captcha object and compare it with the user input. You can choose to ignore case sensitivity by using the equalsIgnoreCase method.

<zscript><![CDATA[
    import org.zkforge.bwcaptcha.Captcha;
    	
    void verifyCaptcha(Textbox tbox,Captcha capt){
        if(!capt.getValue().equals(tbox.getValue())){ throw new WrongValueException(tbox,"Code Error!"); }
    }

    void verifyCaptchaIgnoreCase(Textbox tbox,Captcha capt){
        if(!capt.getValue().equalsIgnoreCase(tbox.getValue())){ throw new WrongValueException(tbox,"Code Error!"); }
    }
]]></zscript>
<vbox>
    <button label="re-generate" onClick="cap1.randomValue();"/>
    <bw.captcha id="cap1" />
    <textbox onChange="verifyCaptcha(self,cap1)" />
    <label value="ignore case"/>
    <textbox onChange="verifyCaptchaIgnoreCase(self,cap1)" />
</vbox>

Here is a quick demo:

Specify how the random value will be generated

Specify the char array to be used for generating a random captcha value and also enter the desired length for the random value.

<zscript><![CDATA[
    char[] captchars = {'0','1','2','3','4','5','6','7','8','9'};
]]></zscript>
<bw.captcha id="cap3" length="6" captchars="${captchars}"/>

A quick demo:

Change style on the bwcaptcha component

You can create a Font object for the bwcaptcha component so that styling elements, such as text font, font color, background color, and line thickness may be customized. Color is set in the format: 0xRRGGBB.

<zscript><![CDATA[
    import java.awt.Font;
    Font font = new Font("Tohama",Font.BOLD,40);
]]></zscript>
<bw.captcha id="cap4" width="200px" height="80px" thickness="7" font="${font}">              
    <attribute name="onCreate"><![CDATA[
        cap4.setBgColor(0x666666);
        cap4.setFontColor(0x00FF00);
        cap4.setFontBgColor(0x55AA55);
        cap4.setLineColor(0xFFFF00);
    ]]></attribute>
</bw.captcha>

A quick demo:

Implement your own algorithm for the bwcaptcha random value

You can implement your own algorithm for generating the bwcaptcha random value and then call self.value(which is equivalent to self.value in this context) to generate the random value.

<zscript><![CDATA[        	
    String generate(){
       /*
            your implementation
        */
    }       
]]></zscript> 
<button label="re-generate" onClick="cap5.value = generate()"/>
<bw.captcha id="cap5" onCreate='self.value = generate()'/>

A quick demo:

Behind the Scene: The Implementation

This component is an image which uses java Graphics2D to draw from automatically generated value.

Create a Graphics2D object

Create a BufferedImage object and give it width and height. Create a Graphics2D object by calling BufferedImage object's createGraphics method and set the background and color.

BufferedImage bi = new BufferedImage(_intWidth, _intHeight, BufferedImage.TYPE_BYTE_INDEXED);
        
Color bgC = new Color(_bgColor);
Color fontC = new Color(_fontColor);
Color lineC = new Color(_lineColor);
        
Graphics2D graphics = bi.createGraphics();
graphics.setBackground(bgC);
graphics.setColor(bgC);

graphics.fillRect(0, 0, bi.getWidth(), bi.getHeight());

Draw the text

First, draw a rectangle for the background. Draw the text twice, one for the foreground text and the other for the background text, then set color on both.

graphics.fillRect(0, 0, bi.getWidth(), bi.getHeight());
Font font = getFont();     

TextLayout textTl = new TextLayout(captchaValue, font, new FontRenderContext(null, true, false));
float x = 10;
float y = (float)(_intHeight - (_intHeight/2 - font.getSize()/2));
graphics.setColor(new Color(_fontBgColor));
textTl.draw(graphics, x+7, y+7);
graphics.setColor(fontC);
textTl.draw(graphics, x, y);

Distort the Text

The generator object generates a period number in random, and redraw the graphic from top to down first ,then from left to right, making the text distorted in a wave-like fashion.

shear(graphics,  bi.getWidth(), bi.getHeight(), bgC);

private void shear(Graphics g, int w1, int h1, Color color) {
    shearX(g, w1, h1, color);
    shearY(g, w1, h1, color);
}
    
private void shearX(Graphics g, int w1, int h1, Color color) {

    int period = generator.nextInt(10) + 5;

    boolean borderGap = true;
    int frames = 15;
    int phase = generator.nextInt(5) + 2;

    for (int i = 0; i < h1; i++) {
        double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (Math.PI*1* (double) phase) / (double) frames);
        g.copyArea(0, i, w1, 1, (int) d, 0);
        if (borderGap) {
            g.setColor(color);
            g.drawLine((int) d, i, 0, i);
            g.drawLine((int) d + w1, i, w1, i);
        }
    }
}

private void shearY(Graphics g, int w1, int h1, Color color) {
    int period = generator.nextInt(30) + 10; // 50;

    boolean borderGap = true;
    int frames = 15;
    int phase = 7;
    for (int i = 0; i < w1; i++) {
        double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (Math.PI*2 * (double) phase) / (double) frames);
        g.copyArea(i, 0, 1, h1, 0, (int) d);
        if (borderGap) {
            g.setColor(color);
            g.drawLine(i, (int) d, i, 0);
            g.drawLine(i, (int) d + h1, i, h1);
        }
    }
}

Draw a thick line

We will give two random coordinates for the points on the left and right edges. The drawThickLine method will compute the points at the corners taken into account the thickness of line, and draw a polygon.

drawThickLine(graphics, 0, generator.nextInt(_intHeight) + 1, _intWidth, generator.nextInt(_intHeight) + 1, _thickness, lineC);

private void drawThickLine(Graphics g, int x1, int y1, int x2, int y2, int thickness, Color c) {
    // The thick line is in fact a filled polygon
    g.setColor(c);
    int dX = x2 - x1;
    int dY = y2 - y1;
    // line length
    double lineLength = Math.sqrt(dX * dX + dY * dY);

    double scale = (double) (thickness) / (2 * lineLength);

    // The x and y increments from an endpoint needed to create a rectangle...
    double ddx = -scale * (double) dY;
    double ddy = scale * (double) dX;
    ddx += (ddx > 0) ? 0.5 : -0.5;
    ddy += (ddy > 0) ? 0.5 : -0.5;
    int dx = (int) ddx;
    int dy = (int) ddy;

    // Now we can compute the corner points...
    int xPoints[] = new int[4];
    int yPoints[] = new int[4];

    xPoints[0] = x1 + dx;
    yPoints[0] = y1 + dy;
    xPoints[1] = x1 - dx;
    yPoints[1] = y1 - dy;
    xPoints[2] = x2 - dx;
    yPoints[2] = y2 - dy;
    xPoints[3] = x2 + dx;
    yPoints[3] = y2 + dy;

    g.fillPolygon(xPoints, yPoints, 4);
}

Generate an Image

Finally, we use CompressQualityParam object to compress the image file, and return it as a byte array. We use this byte array to create an AImage instance, because bwcaptcha extends the zk Image component, we'll just call setContent to put the graphic in content.

ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
    //ImageIO.write(bi,"JPEG", baos);            
            
    ImageOutputStream ios;

    ios = ImageIO.createImageOutputStream(baos);
            
    ImageWriter writer = null;
    Iterator iter = ImageIO.getImageWritersByFormatName("jpg");
    if (iter.hasNext()) {
        writer = (ImageWriter)iter.next();
    }
    
    // Prepare output file
    writer.setOutput(ios);
    
    // Set the compression quality
    CompressQualityParam iwparam = new CompressQualityParam();
    iwparam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT) ;
    iwparam.setCompressionQuality(0.9F);
    
    // Write the image
    writer.write(null, new IIOImage(bi, null, null), iwparam);
    
    // Cleanup
    ios.flush();
    writer.dispose();
    ios.close();
    baos.close();
} catch (IOException e) {
    throw new RuntimeException(e.getMessage(),e);
}

return baos.toByteArray();

private class CompressQualityParam extends JPEGImageWriteParam {
    public CompressQualityParam() {
        super(Locale.getDefault());
    }

    public void setCompressionQuality(float quality) {
        if (quality < 0.0F || quality > 1.0F) {
            throw new IllegalArgumentException("Quality Out-Of-Bounds.");
        }
        this.compressionQuality = 256 - (quality * 256);
    }
}

final AImage media = new AImage("captcha"+new Date().getTime(), bytes);
setContent(media);

Download

Summary

Users have full control of the bwcaptcha component on the server side, without any implementation needed on the client side. For the client, simply extend ZK 5 Image component, and implement how the graphic is to be drawn and then use Image's API to set content.



Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.