View Javadoc

1   /*
2    * JCaptcha, the open source java framework for captcha definition and integration
3    * Copyright (c)  2007 jcaptcha.net. All Rights Reserved.
4    * See the LICENSE.txt file distributed with this package.
5    */
6   
7   package com.octo.captcha.component.image.textpaster;
8   
9   import com.octo.captcha.CaptchaException;
10  import com.octo.captcha.component.image.color.ColorGenerator;
11  
12  import java.awt.*;
13  import java.awt.font.FontRenderContext;
14  import java.awt.font.LineMetrics;
15  import java.awt.font.TextAttribute;
16  import java.awt.font.GlyphVector;
17  import java.awt.geom.Point2D;
18  import java.awt.geom.Rectangle2D;
19  import java.awt.image.BufferedImage;
20  import java.security.SecureRandom;
21  import java.text.AttributedCharacterIterator;
22  import java.text.AttributedString;
23  import java.util.Random;
24  
25  /***
26   * This class is the decomposition of a single AttributedString into its component glyphs. It wouldn't be necessary if
27   * Java2D correctly handled spacing issues with fonts changed AffineTransformation -- there is a possibility that it
28   * will not be necessary with java 1.5
29    * @deprecated 
30   */
31  public class MutableAttributedString {
32  
33      AttributedString originalAttributedString;
34  
35      /***
36       * each character is stored as its own AttributedString
37       */
38      AttributedString[] aStrings;
39  
40      /***
41       * the boundaries are stored as placeholder for placement decisions
42       */
43      Rectangle2D[] bounds;
44  
45      /***
46       * we need the line metrics primarily to get the maximum ascent for all characters.
47       */
48      LineMetrics[] metrics;
49  
50      /***
51       * Glyphs boundaries
52       */
53      GlyphVector[] glyphVectors;
54  
55  
56      /***
57       * Comment for <code>myRandom</code>
58       */
59      private Random myRandom = new SecureRandom();
60  
61      /***
62       * In typography, kerning refers to adjusting the space between characters, especially by placing two characters
63       * closer together than normal. Kerning makes certain combinations of letters, such as WA, MW, TA, and VA, look
64       * better.
65       */
66      private int kerning;
67  
68      /***
69       * Given an attributed string and the graphics environment it lives in, pull it apart into its components.
70       *
71       * @param g2      graphics
72       * @param aString attributed String
73       */
74      protected MutableAttributedString(final Graphics2D g2, AttributedString aString, int kerning) {
75          this.kerning = kerning;
76          this.originalAttributedString=aString;
77          AttributedCharacterIterator iter = aString.getIterator();
78          int n = iter.getEndIndex();
79          aStrings = new AttributedString[n];
80          bounds = new Rectangle2D[n];
81          metrics = new LineMetrics[n];
82  
83          for (int i = iter.getBeginIndex(); i < iter.getEndIndex(); i++) {
84              iter.setIndex(i);
85              aStrings[i] = new AttributedString(iter, i, i + 1);
86              Font font = (Font) iter.getAttribute(TextAttribute.FONT);
87              if (font != null) {
88                  g2.setFont(font); // needed for getFont, -and- getFontRenderContext
89              }
90              final FontRenderContext frc = g2.getFontRenderContext();
91  
92              bounds[i] = g2.getFont().getStringBounds(iter, i, i + 1, frc);
93  
94              metrics[i] = g2.getFont().getLineMetrics((new Character(iter.current())).toString(),
95                      frc);
96        }
97  
98      }
99  
100     /***
101      * Draw all characters according to their computed positions
102      */
103     void drawString(Graphics2D g2) {
104         for (int i = 0; i < length(); i++) {
105             g2.drawString(getIterator(i), (float) getX(i), (float) getY(i));
106         }
107     }
108 
109     /***
110      * Draw all characters according to their computed positions, and a color from the colorGenerator
111      *
112      * @param colorGenerator generate color for each glyph
113      */
114     void drawString(Graphics2D g2, ColorGenerator colorGenerator) {
115         for (int i = 0; i < length(); i++) {
116             g2.setColor(colorGenerator.getNextColor());
117             g2.drawString(getIterator(i), (float) getX(i), (float) getY(i));
118         }
119     }
120 
121     Point2D moveToRandomSpot(final BufferedImage background) {
122         return moveToRandomSpot(background, null);
123     }
124 
125     /***
126      * Given a background image (for size only), pick a random spot such that the entire string can be displayed. This
127      * method implicitly assumes that all resizing issues have been taken care of first. If you resize afterwards, any
128      * type of clipping is possible.
129      *
130      * @param background    the image that will lie under the text
131      * @param startingPoint the suggested starting point, or null if any point is acceptable.
132      * @return a Point2D object indicating the initial starting point of the text
133      * @throws com.octo.captcha.CaptchaException
134      *          if the image size is too small, or the word too long, or the fonts too large.
135      */
136     Point2D moveToRandomSpot(final BufferedImage background, Point2D startingPoint) {
137         int maxHeight = (int) getMaxHeight();
138 
139         // this padding is necessary due to flaws in this algorithm and how it interacts
140         // with java. we are getting the logical bounds of the character, not the actual
141         // bound of the character. So ascenders on rotated characters may extend out of the
142         // box vertically and horizontally (for rotated letters), which means that we can
143         // place the letter such that the final character, f, say, has its top outside the image.
144         // the TextLayout class should be investigated more later; it didn't work well earlier.
145         final int arbitraryHorizontalPadding = 10;
146         final int arbitraryVerticalPadding = 5;
147         double maxX = background.getWidth() - getTotalWidth() - arbitraryHorizontalPadding;
148         double maxY = background.getHeight() - maxHeight - arbitraryVerticalPadding;
149 
150         int newY;
151 
152         if (startingPoint == null) {
153             // we cannot start above the maximum ascent, or below the difference
154             // between text size and image size. nextInt requires values > 0.
155             // no suggested starting point is given - any spot on the vertical axis is ok
156             newY = (int) getMaxAscent() + myRandom.nextInt(Math.max(1, (int) maxY));
157         } else {
158             newY = (int) (startingPoint.getY() + myRandom.nextInt(arbitraryVerticalPadding * 2));
159         }
160 
161         // the bounding box we're using is too small. can we fix the problem?
162         if (maxX < 0 || maxY < 0) {
163             String problem = "too tall:"; // no, we cannot handle this case
164 
165             if (maxX < 0 && maxY > 0) {
166                 problem = "too long:";
167 
168                 // ok, the text slammed into the end of the image. let's try half the kerning:
169                 useMinimumSpacing(kerning / 2);
170                 maxX = background.getWidth() - getTotalWidth();
171                 if (maxX < 0) {
172                     // that didn't work. let's try no kerning
173                     useMinimumSpacing(0);
174 
175                     maxX = background.getWidth() - getTotalWidth();
176                     if (maxX < 0) {
177                         // that didn't work either. let's try gradual steps of negative kerning.
178                         maxX = reduceHorizontalSpacing(background.getWidth(), 0.05 );
179                     }
180                 }
181 
182                 // if one of the above steps worked, then return now;
183                 // otherwise, fall through to exception
184                 if (maxX > 0) {
185                     moveTo(0, newY);
186                     return new Point2D.Float(0, newY);
187                 }
188             }
189 
190             // situtation is unrecoverable -- throw exception
191             throw new CaptchaException("word is " + problem
192                     + " try to use less letters, smaller font" + " or bigger background: "
193                     + " text bounds = " + this + " with fonts " + this.getFontListing()
194                     + " versus image width = " + background.getWidth() + ", height = "
195                     + background.getHeight());
196         }
197 
198         int newX;
199         if (startingPoint == null) {
200             // no suggested starting point - the string can start anywhere horizontal if
201             // the string is long enough
202             newX = myRandom.nextInt(Math.max(1, (int) maxX));
203         } else {
204             newX = (int) (startingPoint.getX() + myRandom.nextInt(arbitraryHorizontalPadding));
205         }
206 
207         moveTo(newX, newY);
208         return new Point2D.Float(newX, newY);
209     }
210 
211     /***
212      * helper method for error message
213      *
214      * @return list of fonts
215      */
216     String getFontListing() {
217         StringBuffer buf = new StringBuffer();
218         final String RS = "\n\t";
219         buf.append("{");
220         for (int i = 0; i < length(); i++) {
221             AttributedCharacterIterator iter = aStrings[i].getIterator();
222             Font font = (Font) iter.getAttribute(TextAttribute.FONT);
223             if (font != null) {
224                 buf.append(font.toString()).append(RS);
225             }
226         }
227         buf.append("}");
228         return buf.toString();
229     }
230 
231     /***
232      * Rearrange the string so that all characters are treated as if they are as wide as the widest character in the
233      * same string.
234      *
235      * @param kerning the space between the characters
236      */
237     void useMonospacing(double kerning) {
238         double maxWidth = getMaxWidth();
239         // for every glyph after the first, space it out so that they are maxWidth characters apart
240         for (int i = 1; i < bounds.length; i++) {
241             // each character between where the previous character ends
242             getBounds(i).setRect(getX(i - 1) + maxWidth + kerning, getY(i), getWidth(i),
243                     getHeight(i));
244         }
245     }
246 
247     /***
248      * Rearrange the string so that all characters are treated as if they are as wide as the widest character in the
249      * same string.
250      *
251      * @param kerning the space between the characters
252      */
253     void useMinimumSpacing(double kerning) {
254 
255         for (int i = 1; i < length(); i++) {
256             bounds[i].setRect(bounds[i - 1].getX() + bounds[i - 1].getWidth() + kerning, bounds[i]
257                     .getY(), bounds[i].getWidth(), bounds[i].getHeight());
258         }
259     }
260 
261     /***
262      * Gradually reduce spacing between letters until the total length is less than the final image width. In many
263      * cases, this will guarantee collisions between the letters.
264      *
265      * @param maxReductionPct maximum percentage reduction
266      * @return if positive, the highest X value that can be safely used for placement of box; if negative, there is no
267      *         safe way to display the text without clipping the ends.
268      */
269     double reduceHorizontalSpacing(int imageWidth, double maxReductionPct) {
270         double maxX = imageWidth - getTotalWidth();
271 
272         double pct = 0;
273         final double stepSize = maxReductionPct / 25;
274         for (pct = stepSize; pct < maxReductionPct && maxX < 0; pct += stepSize) {
275             for (int i = 1; i < length(); i++) {
276                 bounds[i].setRect((1 - pct) * bounds[i].getX(), bounds[i].getY(), bounds[i]
277                         .getWidth(), bounds[i].getHeight());
278             }
279             maxX = (imageWidth - getTotalWidth());
280         }
281         return maxX;
282     }
283 
284 
285      /***
286      * Gradually reduce spacing between letters until the overlap at least equals specified overlapPixs.
287      *
288      * @param overlapPixs
289      * @return if positive, the highest X value that can be safely used for placement of box; if negative, there is no
290      *         safe way to display the text without clipping the ends.
291      */
292     public void overlap(double overlapPixs) {
293         for (int i = 1; i < length(); i++) {
294                 bounds[i].setRect( bounds[i-1].getX()+bounds[i-1].getWidth()-overlapPixs, bounds[i].getY(), bounds[i].getWidth(), bounds[i].getHeight());
295             }
296     }
297 
298     /***
299      * Change the x,y values in the boundaries so they can be used for position.
300      */
301     void moveTo(double newX, double newY) {
302         bounds[0].setRect(newX, newY, bounds[0].getWidth(), bounds[0].getHeight());
303         for (int i = 1; i < length(); i++) {
304             bounds[i].setRect(newX + bounds[i].getX(), newY, bounds[i].getWidth(), bounds[i]
305                     .getHeight());
306         }
307     }
308 
309     /*
310      * shift string to a non-linear layout in the output image
311      */
312     protected void shiftBoundariesToNonLinearLayout(double backgroundWidth, double backgroundHeight) {
313         double newX = backgroundWidth / 20;
314         double middleY = backgroundHeight / 2;
315         Random myRandom = new SecureRandom();
316 
317         bounds[0].setRect(newX, middleY, bounds[0].getWidth(), bounds[0].getHeight());
318         for (int i = 1; i < length(); i++)
319         {
320             double characterHeight = bounds[i].getHeight();
321             double randomY = myRandom.nextInt() % (backgroundHeight / 4);
322             double currentY = middleY + ((myRandom.nextBoolean()) ? randomY : -randomY) + (characterHeight / 4);
323             bounds[i].setRect(newX + bounds[i].getX(), currentY, bounds[i].getWidth(), bounds[i].getHeight());
324         }
325     }
326 
327     public String toString() {
328         StringBuffer buf = new StringBuffer();
329         buf.append("{text=");
330         for (int i = 0; i < length(); i++) {
331             buf.append(aStrings[i].getIterator().current());
332         }
333         final String RS = "\n\t";
334         buf.append(RS);
335         for (int i = 0; i < length(); i++) {
336             buf.append(bounds[i].toString());
337             final String FS = " ";
338             final LineMetrics m = metrics[i];
339             // height = ascent + descent + leading
340             buf.append(" ascent=").append(m.getAscent()).append(FS);
341             buf.append("descent=").append(m.getDescent()).append(FS);
342             buf.append("leading=").append(m.getLeading()).append(FS);
343 
344             buf.append(RS);
345         }
346         buf.append("}");
347         return buf.toString();
348     }
349 
350     public int length() {
351         return bounds.length;
352     }
353 
354     public double getX(int index) {
355         return getBounds(index).getX();
356     }
357 
358     public double getY(int index) {
359         return getBounds(index).getY();
360     }
361 
362     public double getHeight(int index) {
363         return getBounds(index).getHeight();
364     }
365 
366     public double getTotalWidth() {
367         return getX(length() - 1) + getWidth(length() - 1);
368     }
369 
370     public double getWidth(int index) {
371         return getBounds(index).getWidth();
372     }
373 
374     public double getAscent(int index) {
375         return getMetric(index).getAscent();
376     }
377 
378     double getDescent(int index) {
379         return getMetric(index).getDescent();
380     }
381 
382     public double getMaxWidth() {
383         double maxWidth = -1;
384 
385         for (int i = 0; i < bounds.length; i++) {
386             final double w = getWidth(i);
387 
388             if (maxWidth < w) {
389                 maxWidth = w;
390             }
391         }
392         return maxWidth;
393     }
394 
395     public double getMaxAscent() {
396         double maxAscent = -1;
397 
398         for (int i = 0; i < bounds.length; i++) {
399             final double a = getAscent(i);
400 
401             if (maxAscent < a) {
402                 maxAscent = a;
403             }
404         }
405         return maxAscent;
406     }
407 
408     public double getMaxDescent() {
409         double maxDescent = -1;
410 
411         for (int i = 0; i < bounds.length; i++) {
412             final double d = getDescent(i);
413 
414             if (maxDescent < d) {
415                 maxDescent = d;
416             }
417         }
418         return maxDescent;
419     }
420 
421     public double getMaxHeight() {
422         double maxHeight = -1;
423         for (int i = 0; i < bounds.length; i++) {
424             double h = getHeight(i);
425 
426             if (maxHeight < h) {
427                 maxHeight = h;
428             }
429         }
430         return maxHeight;
431     }
432 
433     public double getMaxX() {
434         return getX(0) + getTotalWidth();
435     }
436 
437     public double getMaxY() {
438         return getY(0) + getMaxHeight();
439     }
440 
441     public Rectangle2D getBounds(int index) {
442         return bounds[index];
443     }
444 
445     public LineMetrics getMetric(int index) {
446         return metrics[index];
447     }
448 
449     public AttributedCharacterIterator getIterator(int i) {
450         return aStrings[i].getIterator();
451     }
452 
453 }