Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

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}