1
2
3
4
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);
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
140
141
142
143
144
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
154
155
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
162 if (maxX < 0 || maxY < 0) {
163 String problem = "too tall:";
164
165 if (maxX < 0 && maxY > 0) {
166 problem = "too long:";
167
168
169 useMinimumSpacing(kerning / 2);
170 maxX = background.getWidth() - getTotalWidth();
171 if (maxX < 0) {
172
173 useMinimumSpacing(0);
174
175 maxX = background.getWidth() - getTotalWidth();
176 if (maxX < 0) {
177
178 maxX = reduceHorizontalSpacing(background.getWidth(), 0.05 );
179 }
180 }
181
182
183
184 if (maxX > 0) {
185 moveTo(0, newY);
186 return new Point2D.Float(0, newY);
187 }
188 }
189
190
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
201
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
240 for (int i = 1; i < bounds.length; i++) {
241
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
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
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 }