/*
* JCaptcha, the open source java framework for captcha definition and integration
* Copyright (c) 2007 jcaptcha.net. All Rights Reserved.
* See the LICENSE.txt file distributed with this package.
*/
package com.octo.captcha.component.image.textpaster;
import com.octo.captcha.CaptchaException;
import com.octo.captcha.component.image.color.ColorGenerator;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.font.TextAttribute;
import java.awt.font.GlyphVector;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.security.SecureRandom;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.Random;
/**
* This class is the decomposition of a single AttributedString into its component glyphs. It wouldn't be necessary if
* Java2D correctly handled spacing issues with fonts changed AffineTransformation -- there is a possibility that it
* will not be necessary with java 1.5
* @deprecated
*/
public class MutableAttributedString {
AttributedString originalAttributedString;
/**
* each character is stored as its own AttributedString
*/
AttributedString[] aStrings;
/**
* the boundaries are stored as placeholder for placement decisions
*/
Rectangle2D[] bounds;
/**
* we need the line metrics primarily to get the maximum ascent for all characters.
*/
LineMetrics[] metrics;
/**
* Glyphs boundaries
*/
GlyphVector[] glyphVectors;
/**
* Comment for <code>myRandom</code>
*/
private Random myRandom = new SecureRandom();
/**
* In typography, kerning refers to adjusting the space between characters, especially by placing two characters
* closer together than normal. Kerning makes certain combinations of letters, such as WA, MW, TA, and VA, look
* better.
*/
private int kerning;
/**
* Given an attributed string and the graphics environment it lives in, pull it apart into its components.
*
* @param g2 graphics
* @param aString attributed String
*/
protected MutableAttributedString(final Graphics2D g2, AttributedString aString, int kerning) {
this.kerning = kerning;
this.originalAttributedString=aString;
AttributedCharacterIterator iter = aString.getIterator();
int n = iter.getEndIndex();
aStrings = new AttributedString[n];
bounds = new Rectangle2D[n];
metrics = new LineMetrics[n];
for (int i = iter.getBeginIndex(); i < iter.getEndIndex(); i++) {
iter.setIndex(i);
aStrings[i] = new AttributedString(iter, i, i + 1);
Font font = (Font) iter.getAttribute(TextAttribute.FONT);
if (font != null) {
g2.setFont(font); // needed for getFont, -and- getFontRenderContext
}
final FontRenderContext frc = g2.getFontRenderContext();
bounds[i] = g2.getFont().getStringBounds(iter, i, i + 1, frc);
metrics[i] = g2.getFont().getLineMetrics((new Character(iter.current())).toString(),
frc);
}
}
/**
* Draw all characters according to their computed positions
*/
void drawString(Graphics2D g2) {
for (int i = 0; i < length(); i++) {
g2.drawString(getIterator(i), (float) getX(i), (float) getY(i));
}
}
/**
* Draw all characters according to their computed positions, and a color from the colorGenerator
*
* @param colorGenerator generate color for each glyph
*/
void drawString(Graphics2D g2, ColorGenerator colorGenerator) {
for (int i = 0; i < length(); i++) {
g2.setColor(colorGenerator.getNextColor());
g2.drawString(getIterator(i), (float) getX(i), (float) getY(i));
}
}
Point2D moveToRandomSpot(final BufferedImage background) {
return moveToRandomSpot(background, null);
}
/**
* Given a background image (for size only), pick a random spot such that the entire string can be displayed. This
* method implicitly assumes that all resizing issues have been taken care of first. If you resize afterwards, any
* type of clipping is possible.
*
* @param background the image that will lie under the text
* @param startingPoint the suggested starting point, or null if any point is acceptable.
* @return a Point2D object indicating the initial starting point of the text
* @throws com.octo.captcha.CaptchaException
* if the image size is too small, or the word too long, or the fonts too large.
*/
Point2D moveToRandomSpot(final BufferedImage background, Point2D startingPoint) {
int maxHeight = (int) getMaxHeight();
// this padding is necessary due to flaws in this algorithm and how it interacts
// with java. we are getting the logical bounds of the character, not the actual
// bound of the character. So ascenders on rotated characters may extend out of the
// box vertically and horizontally (for rotated letters), which means that we can
// place the letter such that the final character, f, say, has its top outside the image.
// the TextLayout class should be investigated more later; it didn't work well earlier.
final int arbitraryHorizontalPadding = 10;
final int arbitraryVerticalPadding = 5;
double maxX = background.getWidth() - getTotalWidth() - arbitraryHorizontalPadding;
double maxY = background.getHeight() - maxHeight - arbitraryVerticalPadding;
int newY;
if (startingPoint == null) {
// we cannot start above the maximum ascent, or below the difference
// between text size and image size. nextInt requires values > 0.
// no suggested starting point is given - any spot on the vertical axis is ok
newY = (int) getMaxAscent() + myRandom.nextInt(Math.max(1, (int) maxY));
} else {
newY = (int) (startingPoint.getY() + myRandom.nextInt(arbitraryVerticalPadding * 2));
}
// the bounding box we're using is too small. can we fix the problem?
if (maxX < 0 || maxY < 0) {
String problem = "too tall:"; // no, we cannot handle this case
if (maxX < 0 && maxY > 0) {
problem = "too long:";
// ok, the text slammed into the end of the image. let's try half the kerning:
useMinimumSpacing(kerning / 2);
maxX = background.getWidth() - getTotalWidth();
if (maxX < 0) {
// that didn't work. let's try no kerning
useMinimumSpacing(0);
maxX = background.getWidth() - getTotalWidth();
if (maxX < 0) {
// that didn't work either. let's try gradual steps of negative kerning.
maxX = reduceHorizontalSpacing(background.getWidth(), 0.05 );
}
}
// if one of the above steps worked, then return now;
// otherwise, fall through to exception
if (maxX > 0) {
moveTo(0, newY);
return new Point2D.Float(0, newY);
}
}
// situtation is unrecoverable -- throw exception
throw new CaptchaException("word is " + problem
+ " try to use less letters, smaller font" + " or bigger background: "
+ " text bounds = " + this + " with fonts " + this.getFontListing()
+ " versus image width = " + background.getWidth() + ", height = "
+ background.getHeight());
}
int newX;
if (startingPoint == null) {
// no suggested starting point - the string can start anywhere horizontal if
// the string is long enough
newX = myRandom.nextInt(Math.max(1, (int) maxX));
} else {
newX = (int) (startingPoint.getX() + myRandom.nextInt(arbitraryHorizontalPadding));
}
moveTo(newX, newY);
return new Point2D.Float(newX, newY);
}
/**
* helper method for error message
*
* @return list of fonts
*/
String getFontListing() {
StringBuffer buf = new StringBuffer();
final String RS = "\n\t";
buf.append("{");
for (int i = 0; i < length(); i++) {
AttributedCharacterIterator iter = aStrings[i].getIterator();
Font font = (Font) iter.getAttribute(TextAttribute.FONT);
if (font != null) {
buf.append(font.toString()).append(RS);
}
}
buf.append("}");
return buf.toString();
}
/**
* Rearrange the string so that all characters are treated as if they are as wide as the widest character in the
* same string.
*
* @param kerning the space between the characters
*/
void useMonospacing(double kerning) {
double maxWidth = getMaxWidth();
// for every glyph after the first, space it out so that they are maxWidth characters apart
for (int i = 1; i < bounds.length; i++) {
// each character between where the previous character ends
getBounds(i).setRect(getX(i - 1) + maxWidth + kerning, getY(i), getWidth(i),
getHeight(i));
}
}
/**
* Rearrange the string so that all characters are treated as if they are as wide as the widest character in the
* same string.
*
* @param kerning the space between the characters
*/
void useMinimumSpacing(double kerning) {
for (int i = 1; i < length(); i++) {
bounds[i].setRect(bounds[i - 1].getX() + bounds[i - 1].getWidth() + kerning, bounds[i]
.getY(), bounds[i].getWidth(), bounds[i].getHeight());
}
}
/**
* Gradually reduce spacing between letters until the total length is less than the final image width. In many
* cases, this will guarantee collisions between the letters.
*
* @param maxReductionPct maximum percentage reduction
* @return if positive, the highest X value that can be safely used for placement of box; if negative, there is no
* safe way to display the text without clipping the ends.
*/
double reduceHorizontalSpacing(int imageWidth, double maxReductionPct) {
double maxX = imageWidth - getTotalWidth();
double pct = 0;
final double stepSize = maxReductionPct / 25;
for (pct = stepSize; pct < maxReductionPct && maxX < 0; pct += stepSize) {
for (int i = 1; i < length(); i++) {
bounds[i].setRect((1 - pct) * bounds[i].getX(), bounds[i].getY(), bounds[i]
.getWidth(), bounds[i].getHeight());
}
maxX = (imageWidth - getTotalWidth());
}
return maxX;
}
/**
* Gradually reduce spacing between letters until the overlap at least equals specified overlapPixs.
*
* @param overlapPixs
* @return if positive, the highest X value that can be safely used for placement of box; if negative, there is no
* safe way to display the text without clipping the ends.
*/
public void overlap(double overlapPixs) {
for (int i = 1; i < length(); i++) {
bounds[i].setRect( bounds[i-1].getX()+bounds[i-1].getWidth()-overlapPixs, bounds[i].getY(), bounds[i].getWidth(), bounds[i].getHeight());
}
}
/**
* Change the x,y values in the boundaries so they can be used for position.
*/
void moveTo(double newX, double newY) {
bounds[0].setRect(newX, newY, bounds[0].getWidth(), bounds[0].getHeight());
for (int i = 1; i < length(); i++) {
bounds[i].setRect(newX + bounds[i].getX(), newY, bounds[i].getWidth(), bounds[i]
.getHeight());
}
}
/*
* shift string to a non-linear layout in the output image
*/
protected void shiftBoundariesToNonLinearLayout(double backgroundWidth, double backgroundHeight) {
double newX = backgroundWidth / 20;
double middleY = backgroundHeight / 2;
Random myRandom = new SecureRandom();
bounds[0].setRect(newX, middleY, bounds[0].getWidth(), bounds[0].getHeight());
for (int i = 1; i < length(); i++)
{
double characterHeight = bounds[i].getHeight();
double randomY = myRandom.nextInt() % (backgroundHeight / 4);
double currentY = middleY + ((myRandom.nextBoolean()) ? randomY : -randomY) + (characterHeight / 4);
bounds[i].setRect(newX + bounds[i].getX(), currentY, bounds[i].getWidth(), bounds[i].getHeight());
}
}
public String toString() {
StringBuffer buf = new StringBuffer();
buf.append("{text=");
for (int i = 0; i < length(); i++) {
buf.append(aStrings[i].getIterator().current());
}
final String RS = "\n\t";
buf.append(RS);
for (int i = 0; i < length(); i++) {
buf.append(bounds[i].toString());
final String FS = " ";
final LineMetrics m = metrics[i];
// height = ascent + descent + leading
buf.append(" ascent=").append(m.getAscent()).append(FS);
buf.append("descent=").append(m.getDescent()).append(FS);
buf.append("leading=").append(m.getLeading()).append(FS);
buf.append(RS);
}
buf.append("}");
return buf.toString();
}
public int length() {
return bounds.length;
}
public double getX(int index) {
return getBounds(index).getX();
}
public double getY(int index) {
return getBounds(index).getY();
}
public double getHeight(int index) {
return getBounds(index).getHeight();
}
public double getTotalWidth() {
return getX(length() - 1) + getWidth(length() - 1);
}
public double getWidth(int index) {
return getBounds(index).getWidth();
}
public double getAscent(int index) {
return getMetric(index).getAscent();
}
double getDescent(int index) {
return getMetric(index).getDescent();
}
public double getMaxWidth() {
double maxWidth = -1;
for (int i = 0; i < bounds.length; i++) {
final double w = getWidth(i);
if (maxWidth < w) {
maxWidth = w;
}
}
return maxWidth;
}
public double getMaxAscent() {
double maxAscent = -1;
for (int i = 0; i < bounds.length; i++) {
final double a = getAscent(i);
if (maxAscent < a) {
maxAscent = a;
}
}
return maxAscent;
}
public double getMaxDescent() {
double maxDescent = -1;
for (int i = 0; i < bounds.length; i++) {
final double d = getDescent(i);
if (maxDescent < d) {
maxDescent = d;
}
}
return maxDescent;
}
public double getMaxHeight() {
double maxHeight = -1;
for (int i = 0; i < bounds.length; i++) {
double h = getHeight(i);
if (maxHeight < h) {
maxHeight = h;
}
}
return maxHeight;
}
public double getMaxX() {
return getX(0) + getTotalWidth();
}
public double getMaxY() {
return getY(0) + getMaxHeight();
}
public Rectangle2D getBounds(int index) {
return bounds[index];
}
public LineMetrics getMetric(int index) {
return metrics[index];
}
public AttributedCharacterIterator getIterator(int i) {
return aStrings[i].getIterator();
}
}