001/*
002 * Shredzone Commons
003 *
004 * Copyright (C) 2012 Richard "Shred" Körber
005 *   http://commons.shredzone.org
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU Library General Public License as
009 * published by the Free Software Foundation, either version 3 of the
010 * License, or (at your option) any later version.
011 *
012 * This program is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU Library General Public License
018 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
019 */
020package org.shredzone.commons.captcha.impl;
021
022import static java.lang.Math.PI;
023
024import java.awt.Color;
025import java.awt.Font;
026import java.awt.FontMetrics;
027import java.awt.Graphics2D;
028import java.awt.RenderingHints;
029import java.awt.image.BufferedImage;
030import java.io.InputStream;
031import java.util.Random;
032
033import javax.annotation.PostConstruct;
034
035import org.shredzone.commons.captcha.CaptchaGenerator;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038import org.springframework.beans.factory.annotation.Value;
039import org.springframework.stereotype.Component;
040
041/**
042 * Default implementation of {@link CaptchaGenerator}.
043 *
044 * @author Richard "Shred" Körber
045 */
046@Component("captchaGenerator")
047public class DefaultCaptchaGenerator implements CaptchaGenerator {
048    private static final Logger LOG = LoggerFactory.getLogger(DefaultCaptchaGenerator.class);
049
050    private final Random rnd = new Random();
051
052    @Value("${captcha.width}")
053    private int width;
054
055    @Value("${captcha.height}")
056    private int height;
057
058    @Value("${captcha.fontPath}")
059    private String fontPath;
060
061    @Value("${captcha.fontSize}")
062    private float fontSize;
063
064    @Value("${captcha.grid}")
065    private boolean showGrid;
066
067    private int gridSize = 11;
068    private int rotationAmplitude = 10;
069    private int scaleAmplitude = 20;
070
071    private Font font;
072
073    @Override
074    public int getWidth() { return width; }
075
076    @Override
077    public int getHeight() { return height; }
078
079    /**
080     * Sets up the captcha generator.
081     */
082    @PostConstruct
083    public void setup() {
084        if (width <= 0 || height <= 0) {
085            throw new IllegalStateException("Captcha size is not set");
086        }
087
088        if (fontPath == null) {
089            throw new IllegalStateException("Font is not set");
090        }
091
092        try (InputStream fontStream = DefaultCaptchaGenerator.class.getResourceAsStream(fontPath)) {
093            font = Font.createFont(Font.TRUETYPE_FONT, fontStream);
094        } catch (Exception ex) {
095            LOG.error("Could not open font " + fontPath, ex);
096            throw new IllegalStateException();
097        }
098    }
099
100    @Override
101    public BufferedImage createCaptcha(char[] text) {
102        if (text == null || text.length == 0) {
103            throw new IllegalArgumentException("No captcha text given");
104        }
105
106        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
107        Graphics2D g2d = image.createGraphics();
108        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
109        g2d.setBackground(Color.WHITE);
110        g2d.setColor(Color.BLACK);
111
112        clearCanvas(g2d);
113
114        if (showGrid) {
115            drawGrid(g2d);
116        }
117
118        int charMaxWidth = width / text.length;
119        int xPos = 0;
120        for (char ch : text) {
121            drawCharacter(g2d, ch, xPos, charMaxWidth);
122            xPos += charMaxWidth;
123        }
124
125        g2d.dispose();
126        return image;
127    }
128
129    /**
130     * Clears the canvas.
131     */
132    private void clearCanvas(Graphics2D g2d) {
133        g2d.clearRect(0, 0, width, height);
134    }
135
136    /**
137     * Draws the background grid.
138     */
139    private void drawGrid(Graphics2D g2d) {
140        for (int y = 2; y < height; y += gridSize) {
141            g2d.drawLine(0, y, width - 1, y);
142        }
143
144        for (int x = 2; x < width; x += gridSize) {
145            g2d.drawLine(x, 0, x, height -1);
146        }
147    }
148
149    /**
150     * Draws a single character.
151     *
152     * @param g2d
153     *            {@link Graphics2D} context
154     * @param ch
155     *            character to draw
156     * @param x
157     *            left x position of the character
158     * @param boxWidth
159     *            width of the box
160     */
161    private void drawCharacter(Graphics2D g2d, char ch, int x, int boxWidth) {
162        double degree = (rnd.nextDouble() * rotationAmplitude * 2) - rotationAmplitude;
163        double scale = 1 - (rnd.nextDouble() * scaleAmplitude / 100);
164
165        Graphics2D cg2d = (Graphics2D) g2d.create();
166        cg2d.setFont(font.deriveFont(fontSize));
167
168        cg2d.translate(x + (boxWidth / 2), height / 2);
169        cg2d.rotate(degree * PI / 90);
170        cg2d.scale(scale, scale);
171
172        FontMetrics fm = cg2d.getFontMetrics();
173        int charWidth = fm.charWidth(ch);
174        int charHeight = fm.getAscent() + fm.getDescent();
175
176        cg2d.drawString(String.valueOf(ch), -(charWidth / 2), fm.getAscent() - (charHeight / 2));
177
178        cg2d.dispose();
179    }
180
181}