/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.jetspeed.security.mfa.impl;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.ref.SoftReference;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.TimeZone;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.jetspeed.security.mfa.MFA;
import org.apache.jetspeed.security.mfa.MultiFacetedAuthentication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGImageDecoder;
/**
* TODO: try to find a javax.imageio equivalent and not use Sun classes
* @author <a href="mailto:taylor@apache.org">David Sean Taylor</a>
* @version $Id: $
*/
public final class CaptchaImageResource
{
private static final long serialVersionUID = 1L;
static final Logger logger = LoggerFactory.getLogger(CaptchaImageResource.class);
private String challengeId;
private List charAttsList;
private int height = 0;
private int width = 0;
private byte[] background = null;
private BufferedImage image = null;
private CaptchaConfiguration config;
/** Transient image data so that image only needs to be generated once per VM */
private transient SoftReference imageData;
/**
* Construct.
*/
public CaptchaImageResource(CaptchaConfiguration config)
{
this(config, null);
}
/**
* Construct.
*
* @param challengeId
* The id of the challenge
*/
public CaptchaImageResource(CaptchaConfiguration config, String challengeId)
{
if (challengeId == null)
this.challengeId = randomString(config.getTextMinlength(), config.getTextMaxlength());
else
this.challengeId = challengeId;
this.config = config;
this.background = null;
}
public void setBackgroundImage(byte[] background)
{
this.background = background;
}
/**
* 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;
}
/**
*
*/
public void saveTo(OutputStream target) throws IOException
{
byte[] data = getImageData();
target.write(data);
}
public byte[] getImageBytes()
{
try
{
return getImageData();
}
catch (IOException e)
{
logger.error("Unexpected exception during getImageBytes().", e);
}
return null;
}
/**
* @throws IOException
* @see wicket.markup.html.image.resource.DynamicImageResource#getImageData()
*/
protected final byte[] getImageData() throws IOException
{
// 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);
}
return data;
}
private Font getFont(String fontName)
{
return new Font(fontName, config.getFontStyle(), config.getFontSize());
}
public void init()
{
boolean emptyBackground = true;
if (config.isUseImageBackground() && background != null)
{
ByteArrayInputStream is = new ByteArrayInputStream(background);
JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(is);
try
{
this.image = decoder.decodeAsBufferedImage();
this.width = image.getWidth();
this.height = image.getHeight();
emptyBackground = false;
}
catch (Exception e)
{
emptyBackground = true;
}
}
if (emptyBackground)
{
this.width = config.getTextMarginLeft() * 2;
this.height = config.getTextMarginBottom() * 6;
}
char[] chars = challengeId.toCharArray();
charAttsList = new ArrayList();
TextLayout text = null;
AffineTransform textAt = null;
String []fontNames = config.getFontNames();
for (int i = 0; i < chars.length; i++)
{
// font name
String fontName = (fontNames.length == 1) ? fontNames[0] : fontNames[randomInt(0, fontNames.length)];
// rise
int rise = config.getTextRiseRange();
if (rise > 0)
{
rise = randomInt(config.getTextMarginBottom(), config.getTextMarginBottom() + config.getTextRiseRange());
}
if (config.getTextShear() > 0.0 || config.getTextRotation() > 0)
{
// rotation
double dRotation = 0.0;
if (config.getTextRotation() > 0)
{
dRotation = Math.toRadians(randomInt(-(config.getTextRotation()), config.getTextRotation()));
}
// shear
double shearX = 0.0;
double shearY = 0.0;
if (config.getTextShear() > 0.0)
{
Random ran = new Random();
shearX = ran.nextDouble() * config.getTextShear();
shearY = ran.nextDouble() * config.getTextShear();
}
CharAttributes cf = new CharAttributes(chars[i], fontName, dRotation, rise, shearX, shearY);
charAttsList.add(cf);
text = new TextLayout(chars[i] + "", getFont(fontName),
new FontRenderContext(null, config.isFontAntialiasing(), false));
textAt = new AffineTransform();
if (config.getTextRotation() > 0)
textAt.rotate(dRotation);
if (config.getTextShear() > 0.0)
textAt.shear(shearX, shearY);
}
else
{
CharAttributes cf = new CharAttributes(chars[i], fontName, 0, rise, 0.0, 0.0);
charAttsList.add(cf);
}
if (emptyBackground)
{
Shape shape = text.getOutline(textAt);
// this.width += text.getBounds().getWidth();
this.width += (int) shape.getBounds2D().getWidth();
this.width += config.getTextSpacing() + 1;
if (this.height < (int) shape.getBounds2D().getHeight() + rise)
{
this.height = (int) shape.getBounds2D().getHeight() + rise;
}
}
}
if (emptyBackground)
{
this.image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D gfx = (Graphics2D) this.image.getGraphics();
gfx.setBackground(Color.WHITE);
gfx.clearRect(0, 0, width, height);
}
}
/**
* Renders this image
*
* @return The image data
*/
private final byte[] render() throws IOException
{
Graphics2D gfx = (Graphics2D) this.image.getGraphics();
if (config.isFontAntialiasing())
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int curWidth = config.getTextMarginLeft();
FontRenderContext ctx = new FontRenderContext(null, config.isFontAntialiasing(), false);
for (int i = 0; i < charAttsList.size(); i++)
{
CharAttributes cf = (CharAttributes) charAttsList.get(i);
TextLayout text = new TextLayout(cf.getChar() + "", getFont(cf.getName()), ctx); //gfx.getFontRenderContext());
AffineTransform textAt = new AffineTransform();
textAt.translate(curWidth, this.height - cf.getRise());
if (cf.getRotation() != 0)
{
textAt.rotate(cf.getRotation());
}
if (cf.getShearX() > 0.0)
textAt.shear(cf.getShearX(), cf.getShearY());
Shape shape = text.getOutline(textAt);
curWidth += shape.getBounds().getWidth() + config.getTextSpacing();
if (config.isUseImageBackground())
gfx.setColor(Color.BLACK);
else
gfx.setXORMode(Color.BLACK);
gfx.fill(shape);
}
if (config.isEffectsNoise())
{
noiseEffects(gfx, image);
}
if (config.isUseTimestamp())
{
if (config.isEffectsNoise())
gfx.setColor(Color.WHITE);
else
gfx.setColor(Color.BLACK);
TimeZone tz = TimeZone.getTimeZone(config.getTimestampTZ());
Calendar cal = new GregorianCalendar(tz);
SimpleDateFormat formatter;
if (config.isUseTimestamp24hr())
formatter = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss z");
else
formatter = new SimpleDateFormat("MM/dd/yyyy hh:mm:ss a, z");
formatter.setTimeZone (tz);
Font font = gfx.getFont();
Font newFont = new Font(font.getName(), font.getStyle(), config.getTimestampFontSize());
gfx.setFont(newFont);
gfx.drawString(formatter.format(cal.getTime()), config.getTextMarginLeft() * 4, this.height - 1);
}
return toImageData(image);
}
protected void noiseEffects(Graphics2D gfx, BufferedImage image)
{
// 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.GRAY);
if (config.isFontSizeRandom())
gfx.setStroke(new BasicStroke(randomInt(config.getFontSize() / 8, config.getFontSize() / 2)));
else
gfx.setStroke(new BasicStroke(config.getFontSize()));
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);
}
}
}
/**
* @param image
* The image to turn into data
* @return The image data for this dynamic image
*/
protected byte[] toImageData(final BufferedImage image) throws IOException
{
// Create output stream
final ByteArrayOutputStream out = new ByteArrayOutputStream();
String format = config.getImageFormat().substring(1);
// Get image writer for format
// FIXME: config.getImageFormat()
final ImageWriter writer = (ImageWriter) ImageIO
.getImageWritersByFormatName(format).next();
// Write out image
writer.setOutput(ImageIO.createImageOutputStream(out));
writer.write(image);
// Return the image data
return out.toByteArray();
}
/**
* 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 int randomInt(int min, int max)
{
return (int) (Math.random() * (max - min) + min);
}
public 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 static String randomWord()
{
final String words[] =
{ "Albert", "Barber", "Charlie", "Daniel", "Edward", "Flower",
"Georgia", "Lawrence", "Michael", "Piper", "Stanley"};
return words[randomInt(0, words.length)];
}
public static void main(String args[])
{
String configLocation = "./WebContent/WEB-INF/mfa.properties";
String ttsLocation = "./WebContent/WEB-INF/tts.properties";
PropertiesConfiguration config = new PropertiesConfiguration();
PropertiesConfiguration tconfig = new PropertiesConfiguration();
Properties x = new Properties();
try
{
InputStream is = new FileInputStream(configLocation);
config.load(is);
is.close();
InputStream tis = new FileInputStream(ttsLocation);
tconfig.load(tis);
tis.close();
MultiFacetedAuthentication mfa = new MultiFacetedAuthenticationImpl(config, tconfig);
MFA.setInstance(mfa);
}
catch (Exception e)
{
e.printStackTrace();
System.exit(1);
}
CaptchaConfiguration captchaConfig = new CaptchaConfiguration(config);
CaptchaImageResource captcha = new CaptchaImageResource(captchaConfig);
InputStream is = null;
try
{
is = new FileInputStream("./WebContent/images/jetspeedlogo98.jpg");
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
MultiFacetedAuthenticationImpl.drain(is, bytes);
byte[] background = bytes.toByteArray();
captcha.setBackgroundImage(background);
}
catch (IOException e)
{
captchaConfig.setUseImageBackground(false);
}
finally
{
if (is != null)
{
try
{
is.close();
}
catch (IOException ee)
{}
}
}
captcha.init();
FileOutputStream fs = null;
try
{
fs = new FileOutputStream("/data/result.jpg");
byte[] data = captcha.getImageBytes();
fs.write(data);
}
catch (IOException e)
{
logger.error("Unexpected exception during writing captcha image.", e);
}
finally
{
try
{
if (fs != null) fs.close();
} catch (IOException e)
{
}
}
}
}