Source code: echopoint/image/TextImageReference.java
1 /*
2 * This file is part of the Echo Point Project. This project is a collection
3 * of Components that have extended the Echo Web Application Framework.
4 *
5 * EchoPoint is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * EchoPoint is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with Echo Point; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 */
19
20 package echopoint.image;
21
22 import java.awt.Dimension;
23 import java.awt.Graphics2D;
24 import java.awt.Image;
25 import java.awt.Rectangle;
26 import java.awt.RenderingHints;
27 import java.awt.font.FontRenderContext;
28 import java.awt.font.GraphicAttribute;
29 import java.awt.font.ImageGraphicAttribute;
30 import java.awt.font.TextAttribute;
31 import java.awt.font.TextLayout;
32 import java.awt.geom.AffineTransform;
33 import java.awt.geom.Rectangle2D;
34 import java.awt.image.BufferedImage;
35 import java.io.IOException;
36 import java.io.ObjectInputStream;
37 import java.io.ObjectOutputStream;
38 import java.io.Serializable;
39 import java.net.URL;
40 import java.text.AttributedCharacterIterator;
41 import java.text.AttributedString;
42 import java.text.CharacterIterator;
43 import java.util.ArrayList;
44 import java.util.Map;
45
46 import nextapp.echo.EchoConstants;
47 import nextapp.echo.EchoInstance;
48 import nextapp.echo.Insets;
49
50 import echopoint.util.ColorKit;
51 import echopoint.util.FontKit;
52
53 /**
54 * The <code>TextImageReference</code> class is an ImageReference
55 * implementation that can draw text onto a background image. This makes the
56 * class perfect for "dynamic" button icons since a "button-like" background
57 * image can be used and then button text can be dynamically drawn on it.
58 * <p>
59 * If the text to be rendered wont fit within the background image (plus its Insets)
60 * then the background image can be "scaled" to fit the text. The scaling options
61 * are :
62 * <ul>
63 * <li>SCALE_NONE - no scaling is performed</li>
64 * <li>SCALE_FAST - a fast scaling algorithm is used. Not always the best look but its fast</li>
65 * <li>SCALE_SMOOTH - a better looking scaling algorithm is used at the cost of speed</li>
66 * <li>SCALE_SPLICE_V_THEN_H - this will splice the image at its x,y, first in the vertical direction
67 * and then in the horizontal direction.</li>
68 * <li>SCALE_SPLICE_H_THEN_V - this will splice the image at its x,y, first in the horizontal direction
69 * and then in the vertical direction.</li>
70 * </ul>
71 * <p>
72 * The last two scaling options are very useful for "button-like" background images. This is because
73 * they will not scale the "outer" edges of the image but rather "splice and fill". This means that
74 * "button-like shadows and outlines" will not be lost or scaled unecesarily.
75 * <p>
76 * TextImageReference will also "break" strings on carriage return boundaries, to allow
77 * for multi line text. This may result in the need to "auto-resize" the background image.
78 * <p>
79 * The text is drawn at a specific {x,y} position. If you use a contructor without
80 * the {x,y} parameters, then the text is automatically centred within the
81 * height and width of the backgroundImage. You can use the horizontal and
82 * vertical alignment properties to control where the text is placed in relation
83 * to the {x,y} position. The {x,y} position will be the base line of the text.
84 * <p>
85 * <code>AttributedString</code>'s are used to render the image text. These
86 * contain formatting instructions for specific characters. While the
87 * formatting information uses java.awt.* visual classes, a series of static
88 * helper methods have been provided to help create java.text.AttributedStrings
89 * using Echo color/font objects. You can also use the <code>ColorKit</code>
90 * and <code>FontKit</code> classes.
91 * <p>
92 * <code>AttributedStrings</code> have a great feature whereby the Unicode
93 * replacement character u'FFFC' can be substituted with another visual
94 * element. This is supported via the replacementImages and the
95 * replacementImageAlignment properties. The images in the replacementImages
96 * will be substituted, in a round robin fashion, for each Unicode replacement
97 * character in the AttributedString.
98 * <p>
99 * This class by default does not keep the encoded output image bytes in memory.
100 * You can change this by <code>setKeptInMemory(true).</code>
101 * <p>
102 * You should think about loading the background images via
103 * <code>ImageKit.loadCachedImage(...)</code> so that these static
104 * images are cached even if the dynamic <code>TextImageReference</code>
105 * is not.
106 *
107 * @see java.text.AttributedString
108 * @see java.awt.font.TextAttribute
109 * @see java.awt.Image
110 * @see java.awt.Image#SCALE_FAST
111 * @see java.awt.Image#SCALE_SMOOTH
112 *
113 * @author Brad Baker
114 */
115 public class TextImageReference extends EncodedImageReference implements Serializable {
116
117 /** Dont scale the background image to the text size */
118 public final static int SCALE_NONE = 0;
119
120 /** Choose a scaling algorithm to optimize speed more than smoothness of the scaled image */
121 public final static int SCALE_FAST = 4;
122
123 /** Choose a scaling algorithm to optimize smoothness of the scaled image more than speed */
124 public final static int SCALE_SMOOTH = 8;
125
126 /** Choose a scaling algorithm that splices vertically then horizontally */
127 public final static int SCALE_SPLICE_V_THEN_H = 16;
128
129 /** Choose a scaling algorithm that splices horizontally then vertically */
130 public final static int SCALE_SPLICE_H_THEN_V = 32;
131
132 /** The default scaling option is SCALE_SPLICE_V_THEN_H */
133 public final static int SCALE_DEFAULT = SCALE_SPLICE_V_THEN_H;
134
135 /** The default insets between the text and the outer edge of the background image */
136 public final static Insets DEFAULT_INSETS = new Insets(3);
137
138 private transient AttributedString attributedString;
139 private transient Image backgroundImage;
140 private boolean bestQuality = true;
141 private boolean autoCenterAtConstruction = false;
142 private int horizontalAlignment = EchoConstants.CENTER;
143 private int replacementImageAlignment = EchoConstants.CENTER;
144 private transient Image[] replacementImages;
145 private int textAngle = 0;
146 private int verticalAlignment = EchoConstants.CENTER;
147 private int x = Integer.MAX_VALUE;
148 private int y = Integer.MAX_VALUE;
149 private int origImageW = -1;
150 private int origImageH = -1;
151 private int scaleOption = SCALE_DEFAULT;
152 private Insets insets = DEFAULT_INSETS;
153 private TextImageDrawer drawer = new TextImageDrawer();
154
155 /**
156 * Constructs a TextImageReference that will be drawn centered within the
157 * background image.
158 *
159 * @param string - the attributed string to draw. Can be null.
160 * @param backgroundImage - the background image to draw on. Must not be null.
161 */
162 public TextImageReference(AttributedString string, Image backgroundImage) {
163 super();
164 constructTextImageRefererence(Integer.MAX_VALUE, Integer.MAX_VALUE, string, backgroundImage, SCALE_DEFAULT);
165 }
166
167 /**
168 * Constructs a TextImageReference that will be drawn centered within the
169 * background image and scalled according to the scaling option.
170 *
171 * @param string - the attributed string to draw. Can be null.
172 * @param backgroundImage - the background image to draw on. Must not be null.
173 * @param scaleOption - the scaling option to use
174 */
175 public TextImageReference(AttributedString string, Image backgroundImage, int scaleOption) {
176 super();
177 constructTextImageRefererence(Integer.MAX_VALUE, Integer.MAX_VALUE, string, backgroundImage, scaleOption);
178 }
179
180 /**
181 * Constructs a TextImageReference that will be drawn centered within the
182 * background image, which is loaded from a file.
183 *
184 * @param string - the attributed string to draw. Can be null.
185 * @param imageFileName - the background image file to draw on. Must not be null.
186 */
187 public TextImageReference(AttributedString string, String imageFileName) {
188 super();
189 Image image = ImageKit.loadImage(imageFileName);
190 constructTextImageRefererence(Integer.MAX_VALUE, Integer.MAX_VALUE, string, image, SCALE_DEFAULT);
191 }
192
193 /**
194 * Constructs a TextImageReference that will be centered within the
195 * background image, which is loaded from an URL.
196 *
197 * @param string - the attributed string to draw. Can be null.
198 * @param imageURL - the background image URL to draw on. Must not be null.
199 */
200 public TextImageReference(AttributedString string, URL imageURL) {
201 super();
202 Image image = ImageKit.loadImage(imageURL);
203 constructTextImageRefererence(Integer.MAX_VALUE, Integer.MAX_VALUE, string, image, SCALE_DEFAULT);
204 }
205
206 /**
207 * Constructs a TextImageReference that will be drawn at the x,y points
208 * within the background image.
209 *
210 * @param x - where to draw the text in the x direction
211 * @param y - where to draw the text in the y direction
212 * @param string - the attributed string to draw. Can be null.
213 * @param backgroundImage - the background image to draw on. Must not be null.
214 */
215 public TextImageReference(int x, int y, AttributedString string, Image backgroundImage) {
216 super();
217 constructTextImageRefererence(x, y, string, backgroundImage, SCALE_DEFAULT);
218 }
219
220 /**
221 * Constructs a TextImageReference that will be drawn at the x,y points
222 * within the background image, which is loaded from a file.
223 *
224 * @param x - where to draw the text in the x direction
225 * @param y - where to draw the text in the y direction
226 * @param string - the attributed string to draw. Can be null.
227 * @param imageFileName - the background image to draw on. Must not be null.
228 */
229 public TextImageReference(int x, int y, AttributedString string, String imageFileName) {
230 super();
231 Image image = ImageKit.loadImage(imageFileName);
232 constructTextImageRefererence(x, y, string, image, SCALE_DEFAULT);
233 }
234
235 /**
236 * Constructs a TextImageReference that will be drawn at the x,y points
237 * within the background image, which is loaded from an URL.
238 *
239 * @param x - where to draw the text in the x direction
240 * @param y - where to draw the text in the y direction
241 * @param string - the attributed string to draw. Can be null.
242 * @param imageURL - the background image to draw on. Must not be null.
243 */
244 public TextImageReference(int x, int y, AttributedString string, URL imageURL) {
245 super();
246 Image image = ImageKit.loadImage(imageURL);
247 constructTextImageRefererence(x, y, string, image, SCALE_DEFAULT);
248 }
249
250 /** Constructs the object */
251 private void constructTextImageRefererence(int x, int y, AttributedString string, Image backgroundImage, int scalingOption) {
252 if (backgroundImage == null)
253 throw new IllegalArgumentException("The backgroundImage must be non null");
254 if (!ImageKit.waitForImage(backgroundImage))
255 throw new IllegalStateException("The backgroundImage could not be loaded");
256
257 this.origImageW = backgroundImage.getWidth(ImageKit.imageObserver);
258 this.origImageH = backgroundImage.getHeight(ImageKit.imageObserver);
259 this.scaleOption = scalingOption;
260 this.attributedString = string;
261 this.backgroundImage = backgroundImage;
262 if (x == Integer.MAX_VALUE && y == Integer.MAX_VALUE)
263 this.autoCenterAtConstruction = true;
264
265 // we use GIF encoding by default!
266 super.setEncoder(new GifEncoder());
267 //
268 // initially I thought that keeping the encoded results in memory would
269 // be needed because the encoder would be called all the time. However
270 // the CacheableService facility of Echo is excellent and hence render
271 // will only be called if the image has changed. So we dont keep it
272 // in memory.
273 super.setKeptInMemory(false);
274
275 //
276 // okay workout background image size, and maybe scale it!
277 drawer.reconstructBackgroundImage(x,y);
278 }
279
280
281 /**
282 * Called to paint the text of the TextImageReference. The
283 * background image is already present when this method is called
284 * <p>
285 * Subclasses are expected to draw the AttributedString, respecting
286 * the other parameters that are in force such as textAngle
287 * <p>
288 * This method returns a Rectangle2D that outlines the boundaries
289 * of any text painted, whcih may be larger than the actual
290 * drawing space.
291 *
292 * @param g the Graphics2D
293 * @param x the x co-ord to paint the text
294 * @param y the y co-ord to paint the text
295 * @return returns a boundary Rectangle enclosing all text painted
296 */
297 protected Rectangle2D paintText(Graphics2D g, int x, int y) {
298 return drawer.paintText(g,x,y,this.attributedString);
299 }
300
301 /**
302 * This methods returns an array of TextLayouts that will be used to
303 * draw the attributed string. In this default version of the method,
304 * the TextLayouts are broken down on carriage return boundaries. These
305 * TextLayouts can then be used to determine the height and width
306 * of the attributed string.
307 * <p>
308 * This is called to determine the dimensions of the current text. This
309 * is used to decide whether to "scale" the background image
310 * if the text inside it is bigger then these plus the insets. This
311 * method should return the same dimensions that are used
312 * in the paintText() method.
313 * <p>
314 *
315 * @param g - the Graphics 2D
316 * @param as - the attributed string
317 * @return an array of TextLayout to be used for painting
318 */
319 protected TextLayout[] getTextLayouts(Graphics2D g, AttributedString as) {
320 ACI subcit;
321 ACI cit = new ACI(attributedString.getIterator());
322
323 FontRenderContext frc = g.getFontRenderContext();
324 replaceAttributedImages(g, frc, this.attributedString);
325
326 // run through the string and find all the \n chars indexes.
327 // We start a new line from that position when drawing
328 ArrayList textLayouts = new ArrayList();
329 int current = cit.getBeginIndex();
330 int end = cit.getEndIndex();
331 int previous = current;
332 boolean lastCharWasCR = false;
333 for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
334 if (c == '\n') {
335 if (! lastCharWasCR) {
336 subcit = new ACI(as.getIterator(null,previous,current));
337 textLayouts.add(new TextLayout(subcit.getIterator(), frc));
338 previous = current+1;
339 } else {
340 previous++;
341 }
342 lastCharWasCR = true;
343 } else {
344 lastCharWasCR = false;
345 }
346 current++;
347 }
348 if (previous < end) {
349 subcit = new ACI(as.getIterator(null,previous,end));
350 textLayouts.add(new TextLayout(subcit.getIterator(), frc));
351 }
352
353 return (TextLayout[]) textLayouts.toArray(new TextLayout[textLayouts.size()]);
354 }
355 /**
356 * Replaces all Unicode replacement characters u'FFFC' with the images from
357 * getReplacementImages() array in a round robin fashion.
358 */
359 protected void replaceAttributedImages(Graphics2D g, FontRenderContext frc, AttributedString as) {
360 if (as == null)
361 return;
362
363 StringBuffer buf = new StringBuffer();
364 CharacterIterator cit = as.getIterator();
365 for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
366 buf.append(c);
367 }
368 String s = buf.toString();
369
370 int ii = 0;
371 Image[] images = replacementImages;
372 if (images != null && images.length > 0) {
373
374 // the maximum height
375 int maxTextHeight = 0;
376 for (int i = 0; i < replacementImages.length; i++) {
377 maxTextHeight = Math.max(maxTextHeight, images[i].getHeight(ImageKit.imageObserver));
378 }
379 TextLayout tl = new TextLayout(attributedString.getIterator(), frc);
380 Rectangle rectText = tl.getBounds().getBounds();
381 maxTextHeight = Math.max(maxTextHeight, rectText.height);
382
383 int pos = s.indexOf("\ufffc");
384 while (pos >= 0) {
385 ImageKit.waitForImage(images[ii]);
386
387 //int imgW = images[ii].getWidth(ImageKit.imageObserver);
388 int imgH = images[ii].getHeight(ImageKit.imageObserver);
389 int gX = 0;
390 int gY = 0;
391
392 int attrAlignment = GraphicAttribute.TOP_ALIGNMENT;
393 if (replacementImageAlignment == EchoConstants.TOP) {
394 attrAlignment = GraphicAttribute.ROMAN_BASELINE;
395 gY += rectText.height - tl.getDescent();
396
397 } else if (replacementImageAlignment == EchoConstants.CENTER) {
398 attrAlignment = GraphicAttribute.ROMAN_BASELINE;
399 if (imgH < maxTextHeight) {
400 gY += (rectText.height - tl.getDescent()) / 2 + (imgH / 2);
401 } else {
402 gY += (rectText.height / 2) + (imgH / 2);
403 }
404
405 } else if (replacementImageAlignment == EchoConstants.BOTTOM) {
406 attrAlignment = GraphicAttribute.ROMAN_BASELINE;
407 gY += imgH;
408 }
409 ImageGraphicAttribute imageGraphic = new ImageGraphicAttribute(images[ii], attrAlignment, gX, gY);
410
411 as.addAttribute(TextAttribute.CHAR_REPLACEMENT, imageGraphic, pos, pos + 1);
412
413 // wrap around the replacement images array.
414 ii++;
415 if (ii >= images.length)
416 ii = 0;
417
418 pos = s.indexOf("\ufffc", pos + 1);
419 }
420 }
421 }
422
423
424 /**
425 * We override getImage() so that the resultant image
426 * is "painted" every time it is needed. This is a
427 * computation vs memory tradeoff.
428 *
429 * @see echopoint.image.EncodedImageReference#getImage()
430 */
431 public Image getImage() {
432 Image paintedImage = drawer.paintImage();
433 return paintedImage;
434 }
435 /**
436 * Returns the backgroundImage in use
437 *
438 * @return Image - the backgroundImage in use
439 */
440 public Image getBackgroundImage() {
441 return backgroundImage;
442 }
443
444 /**
445 * This returns the height of the backgroundImage
446 *
447 * @see nextapp.echo.ImageReference#getHeight()
448 */
449 public int getHeight() {
450 return backgroundImage.getHeight(ImageKit.imageObserver);
451 }
452
453 /**
454 * Returns the horizontal alignment of the text relative to the x,y
455 * drawing point.
456 *
457 * @return The horizontal alignment of the text relative to the x,y
458 * drawing point,
459 * one of the following values:
460 * <ul>
461 * <li>EchoConstants.LEFT</li>
462 * <li>EchoConstants.CENTER (the default)</li>
463 * <li>EchoConstants.RIGHT</li>
464 * </ul>
465 */
466 public int getHorizontalAlignment() {
467 return horizontalAlignment;
468 }
469 /**
470 * Returns the alignment of the replacement images within
471 * the <code>AttributedString</code>. This can be
472 * EchoConstants.TOP, EchoConstants.CENTER or EchoConstants.BOTTOM
473 *
474 * @return the replacement image alignment
475 */
476 public int getReplacementImageAlignment() {
477 return replacementImageAlignment;
478 }
479
480 /**
481 * Returns the array of replacement images that will be substituted for
482 * the Unicode replacement character 'u'FFFC'. These are replaced in a
483 * round-robin fashion, by circling around the array. This can be a
484 * null array.
485 *
486 * @return the array of replacement images
487 */
488 public Image[] getReplacementImages() {
489 return replacementImages;
490 }
491
492 /**
493 * Returns the AttributedString in use.
494 *
495 * @return AttributedString - the AttributedString in use.
496 */
497 public AttributedString getString() {
498 return attributedString;
499 }
500
501 /**
502 * Returns the current textAngle.
503 *
504 * @return int - the current textAngle.
505 */
506 public int getTextAngle() {
507 return textAngle;
508 }
509
510 /**
511 * Returns the vertical alignment of the text relative to the x,y
512 * drawing point.
513 *
514 * @return The vertical alignment of the text relative to the x,y
515 * drawing point,
516 * one of the following values:
517 * <ul>
518 * <li>EchoConstants.TOP</li>
519 * <li>EchoConstants.CENTER (the default)</li>
520 * <li>EchoConstants.BOTTOM</li>
521 * </ul>
522 */
523 public int getVerticalAlignment() {
524 return verticalAlignment;
525 }
526
527 /**
528 * This returns the width of the backgroundImage.
529 *
530 * @see nextapp.echo.ImageReference#getWidth()
531 */
532 public int getWidth() {
533 return backgroundImage.getWidth(ImageKit.imageObserver);
534 }
535
536 /**
537 * The x position where the etxt will be drawn.
538 *
539 * @return int - The x position where the etxt will be drawn.
540 */
541 public int getX() {
542 return x;
543 }
544
545 /**
546 * The y position where the etxt will be drawn.
547 *
548 * @return int - The y position where the etxt will be drawn.
549 */
550 public int getY() {
551 return y;
552 }
553
554 /**
555 * Returns true if the image produce is of the best quality.
556 * @return boolean - true if the image produce is of the best quality.
557 */
558 public boolean isBestQuality() {
559 return bestQuality;
560 }
561
562 /**
563 * Sets the background image to be used
564 *
565 * @param image - the background image to be used
566 */
567 public void setBackgroundImage(Image image) {
568 if (image == null)
569 throw new IllegalArgumentException("The background image must be non null");
570
571 Image oldImage = backgroundImage;
572 backgroundImage = image;
573 if (!oldImage.equals(image)) {
574 fireImageChange();
575 }
576
577 }
578
579 /**
580 * Sets the background image by loading from an image
581 * file.
582 *
583 * @param imageFileName - the background image file to be used
584 */
585 public void setBackgroundImage(String imageFileName) {
586 Image image = ImageKit.loadImage(imageFileName);
587 setBackgroundImage(image);
588 }
589
590 /**
591 * Sets the background image by loading from an image
592 * URL.
593 *
594 * @param imageURL - the background image URL to be used
595 */
596 public void setBackgroundImage(URL imageURL) {
597 Image image = ImageKit.loadImage(imageURL);
598 setBackgroundImage(image);
599 }
600
601 /**
602 * Set this to true if you want the best quality images to be drawn.
603 * This setting affects the rendering hints used during image drawing.
604 *
605 * @param b
606 */
607 public void setBestQuality(boolean b) {
608 boolean oldValue = bestQuality;
609 bestQuality = b;
610 if (oldValue != bestQuality) {
611 fireImageChange();
612 }
613 }
614
615 /**
616 * Sets the horizontal alignment of the text relative to the x,y
617 * drawing point.
618 *
619 * @param i - The horizontal alignment of the text relative to the x,y
620 * drawing point,
621 * one of the following values:
622 * <ul>
623 * <li>EchoConstants.LEFT</li>
624 * <li>EchoConstants.CENTER (the default)</li>
625 * <li>EchoConstants.RIGHT</li>
626 * </ul>
627 */
628 public void setHorizontalAlignment(int i) {
629 if (i != EchoConstants.LEFT && i != EchoConstants.CENTER && i != EchoConstants.RIGHT)
630 throw new IllegalArgumentException("setHorizontalAlignment must be one of EchoConstants.LEFT, EchoConstants.CENTER, or EchoConstants.RIGHT");
631
632 int oldValue = horizontalAlignment;
633 horizontalAlignment = i;
634 if (oldValue != horizontalAlignment) {
635 fireImageChange();
636 }
637 }
638
639 /**
640 * Sets the alignment of the replacement images within
641 * the <code>AttributedString</code>. This can be
642 * EchoConstants.TOP, EchoConstants.CENTER or EchoConstants.BOTTOM
643 *
644 * @param i
645 */
646 public void setReplacementImageAlignment(int i) {
647 int oldValue = replacementImageAlignment;
648 replacementImageAlignment = i;
649 if (oldValue != replacementImageAlignment) {
650 fireImageChange();
651 }
652 }
653
654 /**
655 * Sets the array of replacement images that will be substituted for
656 * the Unicode replacement character u'FFFC'. These are replaced in a
657 * round-robin fashion, for each Unicode replacement character in the
658 * AttribuedtString. This can be a null array and hence no replacement
659 * will be done.
660 * @param images
661 * @see AttributedString for more details
662 */
663 public void setReplacementImages(Image[] images) {
664 Image[] oldValue = replacementImages;
665 replacementImages = images;
666 if (oldValue != replacementImages) {
667 fireImageChange();
668 }
669 }
670
671 /**
672 * This is the AttributedString to be drawn onto the
673 * image. This can be null.
674 *
675 * @param string
676 */
677 public void setString(AttributedString string) {
678 AttributedString oldValue = this.attributedString;
679 this.attributedString = string;
680 if (oldValue != null && !oldValue.equals(this.attributedString)) {
681 fireImageChange();
682 }
683 }
684
685 /**
686 * Sets the text angle in degrees to be used.
687 *
688 * @param i int - the text angle to use.,
689 */
690 public void setTextAngle(int i) {
691 int oldValue = textAngle;
692 textAngle = i;
693 if (oldValue != textAngle) {
694 fireImageChange();
695 }
696 }
697 /**
698 * Sets the vertical alignment of the text relative to the x,y
699 * drawing point.
700 *
701 * @param i - The vertical alignment of the text relative to the x,y
702 * drawing point,
703 * one of the following values:
704 * <ul>
705 * <li>EchoConstants.TOP</li>
706 * <li>EchoConstants.CENTER (the default)</li>
707 * <li>EchoConstants.BOTTOM</li>
708 * </ul>
709 */
710 public void setVerticalAlignment(int i) {
711 if (i != EchoConstants.TOP && i != EchoConstants.CENTER && i != EchoConstants.BOTTOM)
712 throw new IllegalArgumentException("setHorizontalAlignment must be one of EchoConstants.TOP, EchoConstants.CENTER, or EchoConstants.BOTTOM");
713
714 int oldValue = verticalAlignment;
715 verticalAlignment = i;
716 if (oldValue != verticalAlignment) {
717 fireImageChange();
718 }
719 }
720
721 /**
722 * Sets the X position to draw the text on the image. If this is set to
723 * Integer.MAX_VALUE then the text will be centered within the
724 * background image.
725 *
726 * @param i
727 */
728 public void setX(int i) {
729 int oldValue = x;
730 x = i;
731 if (oldValue != x) {
732 fireImageChange();
733 }
734 }
735
736 /**
737 * Sets the y position for drawing the text on the image. If this is set to
738 * Integer.MAX_VALUE then the text will be centered within the
739 * background image.
740 *
741 * @param i
742 */
743 public void setY(int i) {
744 int oldValue = y;
745 y = i;
746 if (oldValue != y) {
747 this.autoCenterAtConstruction = false;
748 fireImageChange();
749 }
750 }
751
752 /**
753 * @see java.io.Serializable
754 */
755 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
756 in.defaultReadObject();
757 this.backgroundImage = ImageKit.readSerializedImage(in);
758 this.attributedString = ImageKit.readSerializedAttributedString(in);
759
760 int replaceImagesLen = in.readInt();
761 replacementImages = new Image[replaceImagesLen];
762 for (int i = 0; i < replaceImagesLen; i++) {
763 replacementImages[i] = ImageKit.readSerializedImage(in);
764 }
765 fireImageChange();
766 }
767
768 /**
769 * @see java.io.Serializable
770 */
771 private void writeObject(ObjectOutputStream out) throws IOException {
772 out.defaultWriteObject();
773
774 ImageKit.writeSerializedImage(out, backgroundImage);
775 ImageKit.writeSerializedAttributedString(out, attributedString);
776
777 int replaceImagesLen = (replacementImages == null ? 0 : replacementImages.length);
778 out.writeInt(replaceImagesLen);
779 for (int i = 0; i < replaceImagesLen; i++) {
780 ImageKit.writeSerializedImage(out, replacementImages[i]);
781 }
782 }
783
784 /**
785 * Returns an AttributedString with no color or font information
786 *
787 * @param s
788 * @return AttributedString
789 */
790 public static AttributedString getAttributedString(String s) {
791 return getAttributedString(s, null, null);
792 }
793
794 /**
795 * Returns an AttributedString with the specified forground color
796 * and font based on nextapp.echo.* visual objects.
797 *
798 * @param s - must not be null
799 * @param foreground - can be null
800 * @param font - can be null
801 * @return AttributedString
802 */
803 public static AttributedString getAttributedString(String s, nextapp.echo.Color foreground, nextapp.echo.Font font) {
804 AttributedString as = new AttributedString(s);
805 //
806 // you can add attributes to a zero lengthed string so dont try!
807 if (s == null || s.length() == 0)
808 return as;
809
810 if (foreground != null) {
811 as.addAttribute(TextAttribute.FOREGROUND, ColorKit.makeAwtColor(foreground, null));
812 }
813 if (font != null) {
814 java.awt.Font defaultFont = FontKit.makeAwtFont(EchoInstance.DEFAULT_FONT, null);
815 as.addAttribute(TextAttribute.FONT, FontKit.makeAwtFont(font, defaultFont));
816 }
817 return as;
818 }
819
820 /**
821 * Returns the scaling option in effect.
822 * @return the scaling option in effect.
823 */
824 public int getScaleOption() {
825 return scaleOption;
826 }
827
828 /**
829 * Sets the scaling option that will be used to draw the background
830 * image. If the text of the TextImageReference is to large for the
831 * background image, then the image will be scaled up to the requireed
832 * size, using this scaling option.
833 *
834 * @param scalingOption the new scaling option
835 */
836 public void setScaleOption(int scalingOption) {
837 int oldValue = this.scaleOption;
838 this.scaleOption = scalingOption;
839 if ( oldValue != scalingOption) {
840 fireImageChange();
841 }
842 }
843
844 /**
845 * Returns the Insets in use
846 * @return the Insets in use
847 */
848 public Insets getInsets() {
849 return insets;
850 }
851
852 /**
853 * Sets the Insets between the text and the background image outer edge.
854 * @param insets the Insets between the text and the background image outer edge.
855 */
856 public void setInsets(Insets insets) {
857 Insets oldValue = this.insets;
858 this.insets = insets;
859 if (oldValue != insets) {
860 fireImageChange();
861 }
862 }
863
864 /**
865 * Called when the image properties have changed and the background
866 * image might have to be scaled.
867 */
868 private void fireImageChange() {
869 drawer.reconstructBackgroundImage(this.x,this.y);
870 update();
871 }
872
873 /**
874 * @see java.lang.Object#toString()
875 */
876 public String toString() {
877 StringBuffer sb = new StringBuffer();
878 if (this.attributedString != null) {
879 sb.append(new ACI(this.attributedString.getIterator()).toString());
880 sb.append(" origW:");
881 sb.append(origImageW);
882 sb.append(" origH:");
883 sb.append(origImageH);
884 return sb.toString();
885 } else {
886 return "null";
887 }
888 }
889
890 /**
891 * <code>TextImageDrawer</code> holds all the drawing code, just
892 * to help us organise this java source file.
893 */
894 private class TextImageDrawer implements Serializable {
895
896 /**
897 * This creates a "tall" attributed string version of the
898 * specified string. It does this by replacing all
899 * standard ASCI characters with M and y so that any
900 * scaling will occur as if the string contained a
901 * M and a y. The reason this is important is that
902 * images heights will be consistent accross different
903 * text in the images.
904 */
905 private AttributedString createTallVersion(AttributedString as) {
906 if (as == null)
907 return as;
908
909 AttributedCharacterIterator cit;
910 cit = as.getIterator();
911 for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
912 if (c == 'M' || c == 'y')
913 return as;
914 }
915
916 int count = 0;
917 StringBuffer buf = new StringBuffer();
918 cit = as.getIterator();
919 for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
920 if (Character.isLetter(c)) {
921 c = (count++ % 2 == 0 ? 'y' : 'M');
922 }
923 buf.append(c);
924 }
925
926 AttributedString newAS = new AttributedString(buf.toString());
927 count = 0;
928 cit = as.getIterator();
929 for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
930 Map attributes = cit.getAttributes();
931 newAS.addAttributes(attributes,count,count+1);
932 count++;
933 }
934 return newAS;
935 }
936 /**
937 * Calculates the height of a text layout
938 */
939 private double calcTextLayoutHeight(TextLayout textLayout) {
940 double height = 0;
941 height += textLayout.getAscent();
942 height += textLayout.getDescent();
943 height += textLayout.getLeading();
944 return height;
945 }
946 /**
947 * Gets the bounds of the image with respect to the Insets
948 */
949 Rectangle2D getPreferredBoundsRect(int imageW, int imageH) {
950 int insetL = (getInsets() == null ? 0 : getInsets().getLeft());
951 int insetT = (getInsets() == null ? 0 : getInsets().getTop());
952 int insetR = (getInsets() == null ? 0 : getInsets().getRight());
953 int insetB = (getInsets() == null ? 0 : getInsets().getBottom());
954 Rectangle2D preferredTB = new Rectangle2D.Float(insetL,insetT,imageW-insetR-insetL,imageH-insetB-insetT);
955 return preferredTB;
956 }
957 /**
958 * Returns the total dimensions of a list of TextLayouts.
959 *
960 * @return Dimension - the text dimensions
961 */
962 private Dimension getTextDimensions(TextLayout[] textLayouts) {
963 double height = 0;
964 double width = 0;
965 double w = 0;
966 for (int i = 0; i < textLayouts.length; i++) {
967 w = textLayouts[i].getBounds().getWidth();
968 if (w > width)
969 width = w;
970 height += calcTextLayoutHeight(textLayouts[i]);
971 }
972 return new Dimension((int) width, (int) height);
973 }
974 /**
975 * Paints a small cross hair at a point with a little marker to to
976 * indicate which way postive x and postiive y are.
977 */
978 void _paintCrossHairs(Graphics2D g, float x1, float y1, java.awt.Color clr) {
979 int x = (int) x1;
980 int y = (int) y1;
981
982 int delta = 15;
983 g.setPaint(clr);
984 g.drawLine(x, y, x, y - delta);
985 g.drawLine(x, y, x + delta, y);
986 g.drawLine(x, y, x, y + delta);
987 g.drawLine(x, y, x - delta, y);
988
989 g.drawLine(x, y, x + (delta / 2), y + (delta / 2));
990 }
991 /**
992 * Draws a text layout around a point, aligning the text box
993 * around that point according to the given alignment values and
994 * rotating the text are the given angle.
995 * <p>
996 * This returns a bounding Rectangle of where to text was painted.
997 */
998 private Rectangle2D paintRotatedTextCentered(Graphics2D g, TextLayout tl, float rx, float ry, int textAngle, int hTextAlignment, int vTextAlignment) {
999 Rectangle rect = tl.getBounds().getBounds();
1000
1001 // debug - help us visually to see the {x,y} position
1002 //_paintCrossHairs(g, rx, ry, java.awt.Color.RED);
1003
1004 // save the current transform
1005 AffineTransform atPrev = g.getTransform();
1006
1007 // text is draw from the base line so adjust to its
1008 // logical bounding box, which is what we want centered
1009
1010 float tx = rx;
1011 float ty = ry;
1012 if (hTextAlignment == EchoConstants.CENTER)
1013 tx = rx - (rect.width / 2);
1014 if (hTextAlignment == EchoConstants.RIGHT)
1015 tx = rx - (rect.width);
1016
1017 if (vTextAlignment == EchoConstants.CENTER)
1018 ty = ry + (rect.height / 2) - (tl.getDescent() / 2);
1019 if (vTextAlignment == EchoConstants.TOP)
1020 ty = ry + (rect.height - tl.getDescent());
1021
1022
1023 // and make a new one around of point of interest
1024 AffineTransform at = new AffineTransform();
1025 at.setToIdentity();
1026 at.rotate(Math.toRadians(textAngle), rx, ry);
1027 at.translate(tx, ty);
1028 g.setTransform(at);
1029
1030 float dx = 0;
1031 float dy = 0;
1032 tl.draw(g, dx, dy);
1033
1034 // debug - this draws a box around the text
1035 // helps with visualising centering
1036 //g.setColor(java.awt.Color.black);
1037 //g.draw(rect);
1038
1039 g.setTransform(atPrev);
1040
1041 Rectangle2D bounds = new Rectangle2D.Double();
1042 bounds.setRect(rect.getX()+tx,
1043 rect.getY()+ty,
1044 rect.getWidth(),
1045 rect.getHeight());
1046 return bounds;
1047 }
1048 /** Called to contruct the actual background image and do any stretching as necessary */
1049 private void reconstructBackgroundImage(int inX, int inY) {
1050 if (!ImageKit.waitForImage(backgroundImage))
1051 throw new IllegalStateException("The backgroundImage could not be scaled");
1052
1053 int imageW = backgroundImage.getWidth(ImageKit.imageObserver);
1054 int imageH = backgroundImage.getHeight(ImageKit.imageObserver);
1055
1056 //
1057 // work out if the painted text will be larger then the
1058 // background image plus its insets. If so we might have to scale the
1059 // background image to fit. We "draw" from the
1060 //
1061 if (getScaleOption() != SCALE_NONE) {
1062 BufferedImage bufImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
1063 Graphics2D g = bufImage.createGraphics();
1064
1065 int insetL = (getInsets() == null ? 0 : getInsets().getLeft());
1066 int insetT = (getInsets() == null ? 0 : getInsets().getTop());
1067 int insetR = (getInsets() == null ? 0 : getInsets().getRight());
1068 int insetB = (getInsets() == null ? 0 : getInsets().getBottom());
1069
1070 Rectangle2D preferredTB = new Rectangle2D.Float(insetL,insetT,imageW-insetR-insetL,imageH-insetB-insetT);
1071
1072 int paintX = (autoCenterAtConstruction || inX == Integer.MAX_VALUE) ? imageW / 2 : inX;
1073 int paintY = (autoCenterAtConstruction || inY == Integer.MAX_VALUE) ? imageH / 2 : inY;
1074
1075 AttributedString tallAS = createTallVersion(attributedString);
1076 Rectangle2D actualTB = paintText(g, paintX, paintY, tallAS);
1077 //
1078 // if the painted text does not fit in the actual background image arae
1079 // then lets scale it
1080 if (! preferredTB.contains(actualTB)) {
1081 int useScalingOption = getScaleOption();
1082 int newW = (int) (actualTB.getWidth()+insetL + insetR);
1083 int newH = (int) (actualTB.getHeight()+insetT + insetB);
1084 //
1085 // and scale it
1086 newW = Math.max(imageW,newW);
1087 newH = Math.max(imageH,newH);
1088
1089 int spliceX = (autoCenterAtConstruction || inX == Integer.MAX_VALUE) ? imageW / 2 : inX;
1090 int spliceY = (autoCenterAtConstruction || inY == Integer.MAX_VALUE) ? imageH / 2 : inY;
1091
1092
1093 if (useScalingOption == SCALE_SPLICE_H_THEN_V) {
1094 backgroundImage = ImageKit.enlargeImageSpliceHoriz(backgroundImage,spliceY,newH);
1095 backgroundImage = ImageKit.enlargeImageSpliceVert(backgroundImage,spliceX,newW);
1096 }
1097 else if (useScalingOption == SCALE_SPLICE_V_THEN_H) {
1098 backgroundImage = ImageKit.enlargeImageSpliceVert(backgroundImage,spliceX,newW);
1099 backgroundImage = ImageKit.enlargeImageSpliceHoriz(backgroundImage,spliceY,newH);
1100 }
1101 else {
1102 backgroundImage = backgroundImage.getScaledInstance(newW,newH,useScalingOption);
1103 }
1104 imageW = newW;
1105 imageH = newH;
1106 }
1107 }
1108 x = (autoCenterAtConstruction || inX == Integer.MAX_VALUE) ? imageW / 2 : inX;
1109 y = (autoCenterAtConstruction || inY == Integer.MAX_VALUE) ? imageH / 2 : inY;
1110 }
1111 /** Called when the image may need to be painted */
1112 private Image paintImage() {
1113
1114 ImageKit.waitForImage(backgroundImage);
1115
1116 int imageW = backgroundImage.getWidth(ImageKit.imageObserver);
1117 int imageH = backgroundImage.getHeight(ImageKit.imageObserver);
1118
1119 BufferedImage bufImage = new BufferedImage(imageW, imageH, BufferedImage.TYPE_INT_ARGB);
1120
1121 Graphics2D g = bufImage.createGraphics();
1122 if (bestQuality) {
1123 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1124
1125 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
1126
1127 g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
1128
1129 } else {
1130 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
1131
1132 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
1133
1134 g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
1135 }
1136
1137 /// draw the background image on to the graphics area!
1138 g.drawImage(backgroundImage, 0, 0, imageW, imageH, ImageKit.imageObserver);
1139
1140 // debug our drawing surface
1141 //g.setColor(java.awt.Color.ORANGE);
1142 //g.drawRect(0, 0, imageW-1, imageH-1);
1143
1144 // call the protected paintText method to allow
1145 // others to do the text painting
1146 paintText(g, x, y, attributedString);
1147
1148
1149 // debug - draw our preferred text boundaries
1150 //g.setColor(java.awt.Color.green);
1151 //g.draw(getPreferredBoundsRect(imageW,imageH));
1152
1153 g.dispose();
1154
1155 return bufImage;
1156 }
1157
1158 private Rectangle2D paintText(Graphics2D g, int x, int y, AttributedString attributedString) {
1159
1160 if (attributedString == null)
1161 return new Rectangle();
1162 ACI cit = new ACI(attributedString.getIterator());
1163 int start = cit.getBeginIndex();
1164 int limit = cit.getEndIndex();
1165 if (start == limit)
1166 return new Rectangle();
1167 FontRenderContext frc = g.getFontRenderContext();
1168 // insert our replacement images
1169 replaceAttributedImages(g, frc, attributedString);
1170
1171 TextLayout[] textLayouts = getTextLayouts(g,attributedString);
1172 Dimension textDimensions = getTextDimensions(textLayouts);
1173 float textHeight = (float) textDimensions.getHeight();
1174 float textWidth = (float) textDimensions.getWidth();
1175 if (textWidth <= 0 || textHeight <= 0)
1176 return new Rectangle();
1177
1178 float fx = x;
1179 float fy = y;
1180 float fydraw = fy;
1181
1182 // debug - help us visually to see the {x,y} position
1183 //_paintCrossHairs(g, fx, fy, java.awt.Color.RED);
1184
1185 //
1186 // if we are centered vertically, then we move up to
1187 // 1/2 the height of the total logical text box.
1188 //
1189 if (verticalAlignment == EchoConstants.CENTER)
1190 fy = fy - (textHeight / 2.0f);
1191
1192 Rectangle2D textBounds = null;
1193 for (int i = 0; i < textLayouts.length; i++) {
1194 fydraw = fy;
1195 //
1196 // again if we are centered vertically, we need to move
1197 // down 1/2 the height of the single text box
1198 if (verticalAlignment == EchoConstants.CENTER)
1199 fydraw += calcTextLayoutHeight(textLayouts[i]) / 2.0;
1200
1201 Rectangle2D singleTextBounds = paintRotatedTextCentered(g, textLayouts[i], fx, fydraw, textAngle, horizontalAlignment, verticalAlignment);
1202 if (textBounds == null)
1203 textBounds = singleTextBounds;
1204 else
1205 textBounds = textBounds.createUnion(singleTextBounds);
1206
1207 fy += calcTextLayoutHeight(textLayouts[i]);
1208 }
1209 // debug - help us visually to see the text boundaries
1210 //g.setColor(java.awt.Color.orange);
1211 //g.draw(textBounds);
1212 return textBounds;
1213 }
1214 };
1215
1216 /**
1217 * An AttributedCharacterIteraror delegate. Used to make debugging easier
1218 * in Eclipse!
1219 */
1220 private class ACI implements Serializable {
1221 AttributedCharacterIterator cit;
1222
1223 private ACI(AttributedCharacterIterator cit) {
1224 this.cit = cit;
1225 }
1226 /**
1227 * @see java.lang.Object#toString()
1228 */
1229 public String toString() {
1230 StringBuffer buf = new StringBuffer();
1231 for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
1232 buf.append(c);
1233 }
1234 return buf.toString();
1235 }
1236
1237 public AttributedCharacterIterator getIterator() {
1238 return cit;
1239 }
1240 protected Object clone() throws CloneNotSupportedException {
1241 return cit.clone();
1242 }
1243 public char current() {
1244 return cit.current();
1245 }
1246 public boolean equals(Object obj) {
1247 return cit.equals(obj);
1248 }
1249 public char first() {
1250 return cit.first();
1251 }
1252 public int getBeginIndex() {
1253 return cit.getBeginIndex();
1254 }
1255 public int getEndIndex() {
1256 return cit.getEndIndex();
1257 }
1258 public int getIndex() {
1259 return cit.getIndex();
1260 }
1261 public char last() {
1262 return cit.last();
1263 }
1264 public char next() {
1265 return cit.next();
1266 }
1267 public char previous() {
1268 return cit.previous();
1269 }
1270 public char setIndex(int position) {
1271 return cit.setIndex(position);
1272 }
1273
1274 }
1275}