echopointng.image.TextImageReference Maven / Gradle / Ivy
Show all versions of ibis-echo2 Show documentation
package echopointng.image;
/*
* This file is part of the Echo Point Project. This project is a collection
* of Components that have extended the Echo Web Application Framework.
*
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*/
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.GraphicAttribute;
import java.awt.font.ImageGraphicAttribute;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.URL;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.CharacterIterator;
import java.util.ArrayList;
import java.util.Map;
import nextapp.echo2.app.Alignment;
import nextapp.echo2.app.Color;
import nextapp.echo2.app.Extent;
import nextapp.echo2.app.Font;
import nextapp.echo2.app.Insets;
import echopointng.util.ColorKit;
import echopointng.util.FontKit;
/**
* The TextImageReference
class is an ImageReference
* implementation that can draw text onto a background image. This makes the
* class perfect for "dynamic" button icons since a "button-like" background
* image can be used and then button text can be dynamically drawn on it.
*
* If the text to be rendered wont fit within the background image (plus its Insets)
* then the background image can be "scaled" to fit the text. The scaling options
* are :
*
* - SCALE_NONE - no scaling is performed
* - SCALE_FAST - a fast scaling algorithm is used. Not always the best look but its fast
* - SCALE_SMOOTH - a better looking scaling algorithm is used at the cost of speed
* - SCALE_SPLICE_V_THEN_H - this will splice the image at its x,y, first in the vertical direction
* and then in the horizontal direction.
* - SCALE_SPLICE_H_THEN_V - this will splice the image at its x,y, first in the horizontal direction
* and then in the vertical direction.
*
*
* The last two scaling options are very useful for "button-like" background images. This is because
* they will not scale the "outer" edges of the image but rather "splice and fill". This means that
* "button-like shadows and outlines" will not be lost or scaled unecesarily.
*
* TextImageReference will also "break" strings on carriage return boundaries, to allow
* for multi line text. This may result in the need to "auto-resize" the background image.
*
* The text is drawn at a specific {x,y} position. If you use a contructor without
* the {x,y} parameters, then the text is automatically centred within the
* height and width of the backgroundImage. You can use the horizontal and
* vertical alignment properties to control where the text is placed in relation
* to the {x,y} position. The {x,y} position will be the base line of the text.
*
* AttributedString
's are used to render the image text. These
* contain formatting instructions for specific characters. While the
* formatting information uses java.awt.* visual classes, a series of static
* helper methods have been provided to help create java.text.AttributedStrings
* using Echo color/font objects. You can also use the ColorKit
* and FontKit
classes.
*
* AttributedStrings
have a great feature whereby the Unicode
* replacement character u'FFFC' can be substituted with another visual
* element. This is supported via the replacementImages and the
* replacementImageAlignment properties. The images in the replacementImages
* will be substituted, in a round robin fashion, for each Unicode replacement
* character in the AttributedString.
*
* This class by default does not keep the encoded output image bytes in memory.
* You can change this by setKeptInMemory(true).
*
* You should think about loading the background images via
* ImageKit.loadCachedImage(...)
so that these static
* images are cached even if the dynamic TextImageReference
* is not.
*
* @see java.text.AttributedString
* @see java.awt.font.TextAttribute
* @see java.awt.Image
* @see java.awt.Image#SCALE_FAST
* @see java.awt.Image#SCALE_SMOOTH
*
* @author Brad Baker
*/
public class TextImageReference extends EncodedImageReference implements Serializable {
/** Dont scale the background image to the text size */
public final static int SCALE_NONE = 0;
/** Choose a scaling algorithm to optimize speed more than smoothness of the scaled image */
public final static int SCALE_FAST = 4;
/** Choose a scaling algorithm to optimize smoothness of the scaled image more than speed */
public final static int SCALE_SMOOTH = 8;
/** Choose a scaling algorithm that splices vertically then horizontally */
public final static int SCALE_SPLICE_V_THEN_H = 16;
/** Choose a scaling algorithm that splices horizontally then vertically */
public final static int SCALE_SPLICE_H_THEN_V = 32;
/** The default scaling option is SCALE_SPLICE_V_THEN_H */
public final static int SCALE_DEFAULT = SCALE_SPLICE_V_THEN_H;
/** The default insets between the text and the outer edge of the background image */
public final static Insets DEFAULT_INSETS = new Insets(3);
private transient AttributedString attributedString;
private transient Image backgroundImage;
private transient Image[] replacementImages;
private boolean bestQuality = true;
private boolean autoCenterAtConstruction = false;
private int horizontalAlignment = Alignment.CENTER;
private int replacementImageAlignment = Alignment.CENTER;
private int textAngle = 0;
private int verticalAlignment = Alignment.CENTER;
private int x = Integer.MAX_VALUE;
private int y = Integer.MAX_VALUE;
private int origImageW = -1;
private int origImageH = -1;
private int scaleOption = SCALE_DEFAULT;
private Insets insets = DEFAULT_INSETS;
private TextImageDrawer drawer = new TextImageDrawer();
/**
* Constructs a TextImageReference that will be drawn centered within the
* background image.
*
* @param string - the attributed string to draw. Can be null.
* @param backgroundImage - the background image to draw on. Must not be null.
*/
public TextImageReference(AttributedString string, Image backgroundImage) {
this(string,backgroundImage,SCALE_DEFAULT);
}
/**
* Constructs a TextImageReference that will be drawn centered within the
* background image and scalled according to the scaling option.
*
* @param string - the attributed string to draw. Can be null.
* @param backgroundImage - the background image to draw on. Must not be null.
* @param scaleOption - the scaling option to use
*/
public TextImageReference(AttributedString string, Image backgroundImage, int scaleOption) {
this(string, backgroundImage, scaleOption, new GifEncoder());
}
/**
* Constructs a TextImageReference that will be drawn centered within the
* background image and scalled according to the scaling option.
*
* @param string - the attributed string to draw. Can be null.
* @param backgroundImage - the background image to draw on. Must not be null.
* @param scaleOption - the scaling option to use
* @param encoder - the Imageencoder to use for this image
*/
public TextImageReference(AttributedString string, Image backgroundImage, int scaleOption, ImageEncoder encoder) {
constructTextImageRefererence(Integer.MAX_VALUE, Integer.MAX_VALUE, string, backgroundImage, scaleOption, encoder);
}
/**
* Constructs a TextImageReference that will be drawn centered within the
* background image, which is loaded from a file.
*
* @param string - the attributed string to draw. Can be null.
* @param imageFileName - the background image file to draw on. Must not be null.
* @param encoder = the ImageEncoder to use
*/
public TextImageReference(AttributedString string, String imageFileName,ImageEncoder encoder) {
this(string,ImageKit.loadImage(imageFileName),SCALE_DEFAULT,encoder);
}
/**
* Constructs a TextImageReference that will be drawn centered within the
* background image, which is loaded from a file.
*
* @param string - the attributed string to draw. Can be null.
* @param imageFileName - the background image file to draw on. Must not be null.
*/
public TextImageReference(AttributedString string, String imageFileName) {
this(string,ImageKit.loadImage(imageFileName));
}
/**
* Constructs a TextImageReference that will be centered within the
* background image, which is loaded from an URL.
*
* @param string - the attributed string to draw. Can be null.
* @param imageURL - the background image URL to draw on. Must not be null.
*/
public TextImageReference(AttributedString string, URL imageURL) {
this(string,ImageKit.loadImage(imageURL));
}
/**
* Constructs a TextImageReference that will be centered within the
* background image, which is loaded from an URL.
*
* @param string - the attributed string to draw. Can be null.
* @param imageURL - the background image URL to draw on. Must not be null.
* @param encoder = the ImageEncoder to use
*/
public TextImageReference(AttributedString string, URL imageURL, ImageEncoder encoder) {
this(string,ImageKit.loadImage(imageURL), SCALE_DEFAULT,encoder);
}
/**
* Constructs a TextImageReference that will be drawn at the x,y points
* within the background image.
*
* @param x - where to draw the text in the x direction
* @param y - where to draw the text in the y direction
* @param string - the attributed string to draw. Can be null.
* @param backgroundImage - the background image to draw on. Must not be null.
*/
public TextImageReference(int x, int y, AttributedString string, Image backgroundImage) {
this(x, y, string, backgroundImage, new GifEncoder());
}
/**
* Constructs a TextImageReference that will be drawn at the x,y points
* within the background image.
*
* @param x - where to draw the text in the x direction
* @param y - where to draw the text in the y direction
* @param string - the attributed string to draw. Can be null.
* @param backgroundImage - the background image to draw on. Must not be null.
* @param encoder = the ImageEncoder to use
*/
public TextImageReference(int x, int y, AttributedString string, Image backgroundImage, ImageEncoder encoder) {
constructTextImageRefererence(x, y, string, backgroundImage, SCALE_DEFAULT, encoder);
}
/**
* Constructs a TextImageReference that will be drawn at the x,y points
* within the background image, which is loaded from a file.
*
* @param x - where to draw the text in the x direction
* @param y - where to draw the text in the y direction
* @param string - the attributed string to draw. Can be null.
* @param imageFileName - the background image to draw on. Must not be null.
*/
public TextImageReference(int x, int y, AttributedString string, String imageFileName) {
this(x,y,string,ImageKit.loadImage(imageFileName));
}
/**
* Constructs a TextImageReference that will be drawn at the x,y points
* within the background image, which is loaded from a file.
*
* @param x - where to draw the text in the x direction
* @param y - where to draw the text in the y direction
* @param string - the attributed string to draw. Can be null.
* @param imageFileName - the background image to draw on. Must not be null.
* @param encoder = the ImageEncoder to use
*/
public TextImageReference(int x, int y, AttributedString string, String imageFileName, ImageEncoder encoder) {
this(x,y,string,ImageKit.loadImage(imageFileName),encoder);
}
/**
* Constructs a TextImageReference that will be drawn at the x,y points
* within the background image, which is loaded from an URL.
*
* @param x - where to draw the text in the x direction
* @param y - where to draw the text in the y direction
* @param string - the attributed string to draw. Can be null.
* @param imageURL - the background image to draw on. Must not be null.
*/
public TextImageReference(int x, int y, AttributedString string, URL imageURL) {
this(x,y,string,ImageKit.loadImage(imageURL));
}
/**
* Constructs a TextImageReference that will be drawn at the x,y points
* within the background image, which is loaded from an URL.
*
* @param x - where to draw the text in the x direction
* @param y - where to draw the text in the y direction
* @param string - the attributed string to draw. Can be null.
* @param imageURL - the background image to draw on. Must not be null.
* @param encoder = the ImageEncoder to use
*/
public TextImageReference(int x, int y, AttributedString string, URL imageURL, ImageEncoder encoder) {
this(x,y,string,ImageKit.loadImage(imageURL),encoder);
}
/** Constructs the object */
private void constructTextImageRefererence(int x, int y, AttributedString string, Image backgroundImage, int scalingOption, ImageEncoder encoder) {
if (backgroundImage == null)
throw new IllegalArgumentException("The backgroundImage must be non null");
if (!ImageKit.waitForImage(backgroundImage))
throw new IllegalStateException("The backgroundImage could not be loaded");
this.origImageW = backgroundImage.getWidth(ImageKit.imageObserver);
this.origImageH = backgroundImage.getHeight(ImageKit.imageObserver);
this.scaleOption = scalingOption;
this.attributedString = string;
this.backgroundImage = backgroundImage;
if (x == Integer.MAX_VALUE && y == Integer.MAX_VALUE)
this.autoCenterAtConstruction = true;
if (encoder == null) {
// we use GIF encoding by default!
encoder = new GifEncoder();
}
super.setEncoder(encoder);
//
// initially I thought that keeping the encoded results in memory would
// be needed because the encoder would be called all the time. However
// the CacheableService facility of Echo is excellent and hence render
// will only be called if the image has changed. So we dont keep it
// in memory.
super.setKeptInMemory(false);
//
// okay workout background image size, and maybe scale it!
drawer.reconstructBackgroundImage(x,y);
}
/**
* Called to paint the text of the TextImageReference. The
* background image is already present when this method is called
*
* Subclasses are expected to draw the AttributedString, respecting
* the other parameters that are in force such as textAngle
*
* This method returns a Rectangle2D that outlines the boundaries
* of any text painted, whcih may be larger than the actual
* drawing space.
*
* @param g the Graphics2D
* @param x the x co-ord to paint the text
* @param y the y co-ord to paint the text
* @return returns a boundary Rectangle enclosing all text painted
*/
protected Rectangle2D paintText(Graphics2D g, int x, int y) {
return drawer.paintText(g,x,y,this.attributedString);
}
/**
* This methods returns an array of TextLayouts that will be used to
* draw the attributed string. In this default version of the method,
* the TextLayouts are broken down on carriage return boundaries. These
* TextLayouts can then be used to determine the height and width
* of the attributed string.
*
* This is called to determine the dimensions of the current text. This
* is used to decide whether to "scale" the background image
* if the text inside it is bigger then these plus the insets. This
* method should return the same dimensions that are used
* in the paintText() method.
*
*
* @param g - the Graphics 2D
* @param as - the attributed string
* @return an array of TextLayout to be used for painting
*/
protected TextLayout[] getTextLayouts(Graphics2D g, AttributedString as) {
ACI subcit;
ACI cit = new ACI(attributedString.getIterator());
FontRenderContext frc = g.getFontRenderContext();
replaceAttributedImages(g, frc, this.attributedString);
// run through the string and find all the \n chars indexes.
// We start a new line from that position when drawing
ArrayList textLayouts = new ArrayList();
int current = cit.getBeginIndex();
int end = cit.getEndIndex();
int previous = current;
boolean lastCharWasCR = false;
for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
if (c == '\n') {
if (! lastCharWasCR) {
subcit = new ACI(as.getIterator(null,previous,current));
textLayouts.add(new TextLayout(subcit.getIterator(), frc));
previous = current+1;
} else {
previous++;
}
lastCharWasCR = true;
} else {
lastCharWasCR = false;
}
current++;
}
if (previous < end) {
subcit = new ACI(as.getIterator(null,previous,end));
textLayouts.add(new TextLayout(subcit.getIterator(), frc));
}
return (TextLayout[]) textLayouts.toArray(new TextLayout[textLayouts.size()]);
}
/**
* Replaces all Unicode replacement characters u'FFFC' with the images from
* getReplacementImages() array in a round robin fashion.
*/
protected void replaceAttributedImages(Graphics2D g, FontRenderContext frc, AttributedString as) {
if (as == null)
return;
StringBuffer buf = new StringBuffer();
CharacterIterator cit = as.getIterator();
for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
buf.append(c);
}
String s = buf.toString();
int ii = 0;
Image[] images = replacementImages;
if (images != null && images.length > 0) {
// the maximum height
int maxTextHeight = 0;
for (int i = 0; i < replacementImages.length; i++) {
maxTextHeight = Math.max(maxTextHeight, images[i].getHeight(ImageKit.imageObserver));
}
TextLayout tl = new TextLayout(attributedString.getIterator(), frc);
Rectangle rectText = tl.getBounds().getBounds();
maxTextHeight = Math.max(maxTextHeight, rectText.height);
int pos = s.indexOf("\ufffc");
while (pos >= 0) {
ImageKit.waitForImage(images[ii]);
//int imgW = images[ii].getWidth(ImageKit.imageObserver);
int imgH = images[ii].getHeight(ImageKit.imageObserver);
int gX = 0;
int gY = 0;
int attrAlignment = GraphicAttribute.TOP_ALIGNMENT;
if (replacementImageAlignment == Alignment.TOP) {
attrAlignment = GraphicAttribute.ROMAN_BASELINE;
gY += rectText.height - tl.getDescent();
} else if (replacementImageAlignment == Alignment.CENTER) {
attrAlignment = GraphicAttribute.ROMAN_BASELINE;
if (imgH < maxTextHeight) {
gY += (rectText.height - tl.getDescent()) / 2 + (imgH / 2);
} else {
gY += (rectText.height / 2) + (imgH / 2);
}
} else if (replacementImageAlignment == Alignment.BOTTOM) {
attrAlignment = GraphicAttribute.ROMAN_BASELINE;
gY += imgH;
}
ImageGraphicAttribute imageGraphic = new ImageGraphicAttribute(images[ii], attrAlignment, gX, gY);
as.addAttribute(TextAttribute.CHAR_REPLACEMENT, imageGraphic, pos, pos + 1);
// wrap around the replacement images array.
ii++;
if (ii >= images.length)
ii = 0;
pos = s.indexOf("\ufffc", pos + 1);
}
}
}
/**
* We override getImage() so that the resultant image
* is "painted" every time it is needed. This is a
* computation vs memory tradeoff.
*
* @see EncodedImageReference#getImage()
*/
public Image getImage() {
Image paintedImage = drawer.paintImage();
return paintedImage;
}
/**
* Returns the backgroundImage in use
*
* @return Image - the backgroundImage in use
*/
public Image getBackgroundImage() {
return backgroundImage;
}
/**
* This returns the height of the backgroundImage
*/
public Extent getHeight() {
return new Extent(backgroundImage.getHeight(ImageKit.imageObserver));
}
/**
* Returns the horizontal alignment of the text relative to the x,y
* drawing point.
*
* @return The horizontal alignment of the text relative to the x,y
* drawing point,
* one of the following values:
*
* - Alignment.LEFT
* - Alignment.CENTER (the default)
* - Alignment.RIGHT
*
*/
public int getHorizontalAlignment() {
return horizontalAlignment;
}
/**
* Returns the alignment of the replacement images within
* the AttributedString
. This can be
* Alignment.TOP, Alignment.CENTER or Alignment.BOTTOM
*
* @return the replacement image alignment
*/
public int getReplacementImageAlignment() {
return replacementImageAlignment;
}
/**
* Returns the array of replacement images that will be substituted for
* the Unicode replacement character 'u'FFFC'. These are replaced in a
* round-robin fashion, by circling around the array. This can be a
* null array.
*
* @return the array of replacement images
*/
public Image[] getReplacementImages() {
return replacementImages;
}
/**
* Returns the AttributedString in use.
*
* @return AttributedString - the AttributedString in use.
*/
public AttributedString getString() {
return attributedString;
}
/**
* Returns the current textAngle.
*
* @return int - the current textAngle.
*/
public int getTextAngle() {
return textAngle;
}
/**
* Returns the vertical alignment of the text relative to the x,y
* drawing point.
*
* @return The vertical alignment of the text relative to the x,y
* drawing point,
* one of the following values:
*
* - Alignment.TOP
* - Alignment.CENTER (the default)
* - Alignment.BOTTOM
*
*/
public int getVerticalAlignment() {
return verticalAlignment;
}
/**
* This returns the width of the backgroundImage.
*/
public Extent getWidth() {
return new Extent(backgroundImage.getWidth(ImageKit.imageObserver));
}
/**
* The x position where the etxt will be drawn.
*
* @return int - The x position where the etxt will be drawn.
*/
public int getX() {
return x;
}
/**
* The y position where the etxt will be drawn.
*
* @return int - The y position where the etxt will be drawn.
*/
public int getY() {
return y;
}
/**
* Returns true if the image produce is of the best quality.
* @return boolean - true if the image produce is of the best quality.
*/
public boolean isBestQuality() {
return bestQuality;
}
/**
* Sets the background image to be used
*
* @param image - the background image to be used
*/
public void setBackgroundImage(Image image) {
if (image == null)
throw new IllegalArgumentException("The background image must be non null");
Image oldImage = backgroundImage;
backgroundImage = image;
if (!oldImage.equals(image)) {
fireImageChange();
}
}
/**
* Sets the background image by loading from an image
* file.
*
* @param imageFileName - the background image file to be used
*/
public void setBackgroundImage(String imageFileName) {
Image image = ImageKit.loadImage(imageFileName);
setBackgroundImage(image);
}
/**
* Sets the background image by loading from an image
* URL.
*
* @param imageURL - the background image URL to be used
*/
public void setBackgroundImage(URL imageURL) {
Image image = ImageKit.loadImage(imageURL);
setBackgroundImage(image);
}
/**
* Set this to true if you want the best quality images to be drawn.
* This setting affects the rendering hints used during image drawing.
*
* @param b
*/
public void setBestQuality(boolean b) {
boolean oldValue = bestQuality;
bestQuality = b;
if (oldValue != bestQuality) {
fireImageChange();
}
}
/**
* Sets the horizontal alignment of the text relative to the x,y
* drawing point.
*
* @param i - The horizontal alignment of the text relative to the x,y
* drawing point,
* one of the following values:
*
* - Alignment.LEFT
* - Alignment.CENTER (the default)
* - Alignment.RIGHT
*
*/
public void setHorizontalAlignment(int i) {
if (i != Alignment.LEFT && i != Alignment.CENTER && i != Alignment.RIGHT)
throw new IllegalArgumentException("setHorizontalAlignment must be one of Alignment.LEFT, Alignment.CENTER, or Alignment.RIGHT");
int oldValue = horizontalAlignment;
horizontalAlignment = i;
if (oldValue != horizontalAlignment) {
fireImageChange();
}
}
/**
* Sets the alignment of the replacement images within
* the AttributedString
. This can be
* Alignment.TOP, Alignment.CENTER or Alignment.BOTTOM
*
* @param i
*/
public void setReplacementImageAlignment(int i) {
int oldValue = replacementImageAlignment;
replacementImageAlignment = i;
if (oldValue != replacementImageAlignment) {
fireImageChange();
}
}
/**
* Sets the array of replacement images that will be substituted for
* the Unicode replacement character u'FFFC'. These are replaced in a
* round-robin fashion, for each Unicode replacement character in the
* AttribuedtString. This can be a null array and hence no replacement
* will be done.
* @param images
* @see AttributedString for more details
*/
public void setReplacementImages(Image[] images) {
Image[] oldValue = replacementImages;
replacementImages = images;
if (oldValue != replacementImages) {
fireImageChange();
}
}
/**
* This is the AttributedString to be drawn onto the
* image. This can be null.
*
* @param string
*/
public void setString(AttributedString string) {
AttributedString oldValue = this.attributedString;
this.attributedString = string;
if (oldValue != null && !oldValue.equals(this.attributedString)) {
fireImageChange();
}
}
/**
* Sets the text angle in degrees to be used.
*
* @param i int - the text angle to use.,
*/
public void setTextAngle(int i) {
int oldValue = textAngle;
textAngle = i;
if (oldValue != textAngle) {
fireImageChange();
}
}
/**
* Sets the vertical alignment of the text relative to the x,y
* drawing point.
*
* @param i - The vertical alignment of the text relative to the x,y
* drawing point,
* one of the following values:
*
* - Alignment.TOP
* - Alignment.CENTER (the default)
* - Alignment.BOTTOM
*
*/
public void setVerticalAlignment(int i) {
if (i != Alignment.TOP && i != Alignment.CENTER && i != Alignment.BOTTOM)
throw new IllegalArgumentException("setHorizontalAlignment must be one of Alignment.TOP, Alignment.CENTER, or Alignment.BOTTOM");
int oldValue = verticalAlignment;
verticalAlignment = i;
if (oldValue != verticalAlignment) {
fireImageChange();
}
}
/**
* Sets the X position to draw the text on the image. If this is set to
* Integer.MAX_VALUE then the text will be centered within the
* background image.
*
* @param i
*/
public void setX(int i) {
int oldValue = x;
x = i;
if (oldValue != x) {
fireImageChange();
}
}
/**
* Sets the y position for drawing the text on the image. If this is set to
* Integer.MAX_VALUE then the text will be centered within the
* background image.
*
* @param i
*/
public void setY(int i) {
int oldValue = y;
y = i;
if (oldValue != y) {
this.autoCenterAtConstruction = false;
fireImageChange();
}
}
/**
* @see java.io.Serializable
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.backgroundImage = ImageKit.readSerializedImage(in);
this.attributedString = ImageKit.readSerializedAttributedString(in);
int replaceImagesLen = in.readInt();
replacementImages = new Image[replaceImagesLen];
for (int i = 0; i < replaceImagesLen; i++) {
replacementImages[i] = ImageKit.readSerializedImage(in);
}
fireImageChange();
}
/**
* @see java.io.Serializable
*/
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
ImageKit.writeSerializedImage(out, backgroundImage);
ImageKit.writeSerializedAttributedString(out, attributedString);
int replaceImagesLen = (replacementImages == null ? 0 : replacementImages.length);
out.writeInt(replaceImagesLen);
for (int i = 0; i < replaceImagesLen; i++) {
ImageKit.writeSerializedImage(out, replacementImages[i]);
}
}
/**
* Returns an AttributedString with no color or font information
*
* @param s
* @return AttributedString
*/
public static AttributedString getAttributedString(String s) {
return getAttributedString(s, null, null);
}
/**
* Returns an AttributedString with the specified forground color
* and font based on nextapp.echo.* visual objects.
*
* @param s - must not be null
* @param foreground - can be null
* @param font - can be null
* @return AttributedString
*/
public static AttributedString getAttributedString(String s, Color foreground, Font font) {
AttributedString as = new AttributedString(s);
//
// you can add attributes to a zero lengthed string so dont try!
if (s == null || s.length() == 0)
return as;
if (foreground != null) {
as.addAttribute(TextAttribute.FOREGROUND, ColorKit.makeAwtColor(foreground, null));
}
if (font != null) {
as.addAttribute(TextAttribute.FONT, FontKit.makeAwtFont(font, null));
}
return as;
}
/**
* Returns the scaling option in effect.
* @return the scaling option in effect.
*/
public int getScaleOption() {
return scaleOption;
}
/**
* Sets the scaling option that will be used to draw the background
* image. If the text of the TextImageReference is to large for the
* background image, then the image will be scaled up to the requireed
* size, using this scaling option.
*
* @param scalingOption the new scaling option
*/
public void setScaleOption(int scalingOption) {
int oldValue = this.scaleOption;
this.scaleOption = scalingOption;
if ( oldValue != scalingOption) {
fireImageChange();
}
}
/**
* Returns the Insets in use
* @return the Insets in use
*/
public Insets getInsets() {
return insets;
}
/**
* Sets the Insets between the text and the background image outer edge.
* @param insets the Insets between the text and the background image outer edge.
*/
public void setInsets(Insets insets) {
Insets oldValue = this.insets;
this.insets = insets;
if (oldValue != insets) {
fireImageChange();
}
}
/**
* Called when the image properties have changed and the background
* image might have to be scaled.
*/
private void fireImageChange() {
drawer.reconstructBackgroundImage(this.x,this.y);
update();
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
StringBuffer sb = new StringBuffer();
if (this.attributedString != null) {
sb.append(new ACI(this.attributedString.getIterator()).toString());
sb.append(" origW:");
sb.append(origImageW);
sb.append(" origH:");
sb.append(origImageH);
return sb.toString();
} else {
return "null";
}
}
/**
* TextImageDrawer
holds all the drawing code, just
* to help us organise this java source file.
*/
private class TextImageDrawer implements Serializable {
/**
* This creates a "tall" attributed string version of the
* specified string. It does this by replacing all
* standard ASCI characters with M and y so that any
* scaling will occur as if the string contained a
* M and a y. The reason this is important is that
* images heights will be consistent accross different
* text in the images.
*/
private AttributedString createTallVersion(AttributedString as) {
if (as == null)
return as;
AttributedCharacterIterator cit;
cit = as.getIterator();
for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
if (c == 'M' || c == 'y')
return as;
}
int count = 0;
StringBuffer buf = new StringBuffer();
cit = as.getIterator();
for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
if (Character.isLetter(c)) {
c = (count++ % 2 == 0 ? 'y' : 'M');
}
buf.append(c);
}
AttributedString newAS = new AttributedString(buf.toString());
count = 0;
cit = as.getIterator();
for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
Map attributes = cit.getAttributes();
newAS.addAttributes(attributes,count,count+1);
count++;
}
return newAS;
}
/**
* Calculates the height of a text layout
*/
private double calcTextLayoutHeight(TextLayout textLayout) {
double height = 0;
height += textLayout.getAscent();
height += textLayout.getDescent();
height += textLayout.getLeading();
return height;
}
/**
* Gets the bounds of the image with respect to the Insets
*/
Rectangle2D getPreferredBoundsRect(int imageW, int imageH) {
int insetL = (getInsets() == null ? 0 : getInsets().getLeft().getValue());
int insetT = (getInsets() == null ? 0 : getInsets().getTop().getValue());
int insetR = (getInsets() == null ? 0 : getInsets().getRight().getValue());
int insetB = (getInsets() == null ? 0 : getInsets().getBottom().getValue());
Rectangle2D preferredTB = new Rectangle2D.Float(insetL,insetT,imageW-insetR-insetL,imageH-insetB-insetT);
return preferredTB;
}
/**
* Returns the total dimensions of a list of TextLayouts.
*
* @return Dimension - the text dimensions
*/
private Dimension getTextDimensions(TextLayout[] textLayouts) {
double height = 0;
double width = 0;
double w = 0;
for (int i = 0; i < textLayouts.length; i++) {
w = textLayouts[i].getBounds().getWidth();
if (w > width)
width = w;
height += calcTextLayoutHeight(textLayouts[i]);
}
return new Dimension((int) width, (int) height);
}
/**
* Paints a small cross hair at a point with a little marker to to
* indicate which way postive x and postiive y are.
*/
void _paintCrossHairs(Graphics2D g, float x1, float y1, java.awt.Color clr) {
int x = (int) x1;
int y = (int) y1;
int delta = 15;
g.setPaint(clr);
g.drawLine(x, y, x, y - delta);
g.drawLine(x, y, x + delta, y);
g.drawLine(x, y, x, y + delta);
g.drawLine(x, y, x - delta, y);
g.drawLine(x, y, x + (delta / 2), y + (delta / 2));
}
/**
* Draws a text layout around a point, aligning the text box
* around that point according to the given alignment values and
* rotating the text are the given angle.
*
* This returns a bounding Rectangle of where to text was painted.
*/
private Rectangle2D paintRotatedTextCentered(Graphics2D g, TextLayout tl, float rx, float ry, int textAngle, int hTextAlignment, int vTextAlignment) {
Rectangle rect = tl.getBounds().getBounds();
// debug - help us visually to see the {x,y} position
//_paintCrossHairs(g, rx, ry, java.awt.Color.RED);
// save the current transform
AffineTransform atPrev = g.getTransform();
// text is draw from the base line so adjust to its
// logical bounding box, which is what we want centered
float tx = rx;
float ty = ry;
if (hTextAlignment == Alignment.CENTER)
tx = rx - (rect.width / 2);
if (hTextAlignment == Alignment.RIGHT)
tx = rx - (rect.width);
if (vTextAlignment == Alignment.CENTER)
ty = ry + (rect.height / 2) - (tl.getDescent() / 2);
if (vTextAlignment == Alignment.TOP)
ty = ry + (rect.height - tl.getDescent());
// and make a new one around of point of interest
AffineTransform at = new AffineTransform();
at.setToIdentity();
at.rotate(Math.toRadians(textAngle), rx, ry);
at.translate(tx, ty);
g.setTransform(at);
float dx = 0;
float dy = 0;
tl.draw(g, dx, dy);
// debug - this draws a box around the text
// helps with visualising centering
//g.setColor(java.awt.Color.black);
//g.draw(rect);
g.setTransform(atPrev);
Rectangle2D bounds = new Rectangle2D.Double();
bounds.setRect(rect.getX()+tx,
rect.getY()+ty,
rect.getWidth(),
rect.getHeight());
return bounds;
}
/** Called to contruct the actual background image and do any stretching as necessary */
private void reconstructBackgroundImage(int inX, int inY) {
if (!ImageKit.waitForImage(backgroundImage))
throw new IllegalStateException("The backgroundImage could not be scaled");
int imageW = backgroundImage.getWidth(ImageKit.imageObserver);
int imageH = backgroundImage.getHeight(ImageKit.imageObserver);
//
// work out if the painted text will be larger then the
// background image plus its insets. If so we might have to scale the
// background image to fit. We "draw" from the
//
if (getScaleOption() != SCALE_NONE) {
BufferedImage bufImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bufImage.createGraphics();
int insetL = (getInsets() == null ? 0 : getInsets().getLeft().getValue());
int insetT = (getInsets() == null ? 0 : getInsets().getTop().getValue());
int insetR = (getInsets() == null ? 0 : getInsets().getRight().getValue());
int insetB = (getInsets() == null ? 0 : getInsets().getBottom().getValue());
Rectangle2D preferredTB = new Rectangle2D.Float(insetL,insetT,imageW-insetR-insetL,imageH-insetB-insetT);
int paintX = (autoCenterAtConstruction || inX == Integer.MAX_VALUE) ? imageW / 2 : inX;
int paintY = (autoCenterAtConstruction || inY == Integer.MAX_VALUE) ? imageH / 2 : inY;
AttributedString tallAS = createTallVersion(attributedString);
Rectangle2D actualTB = paintText(g, paintX, paintY, tallAS);
//
// if the painted text does not fit in the actual background image arae
// then lets scale it
if (! preferredTB.contains(actualTB)) {
int useScalingOption = getScaleOption();
int newW = (int) (actualTB.getWidth()+insetL + insetR);
int newH = (int) (actualTB.getHeight()+insetT + insetB);
//
// and scale it
newW = Math.max(imageW,newW);
newH = Math.max(imageH,newH);
int spliceX = (autoCenterAtConstruction || inX == Integer.MAX_VALUE) ? imageW / 2 : inX;
int spliceY = (autoCenterAtConstruction || inY == Integer.MAX_VALUE) ? imageH / 2 : inY;
if (useScalingOption == SCALE_SPLICE_H_THEN_V) {
backgroundImage = ImageKit.enlargeImageSpliceHoriz(backgroundImage,spliceY,newH);
backgroundImage = ImageKit.enlargeImageSpliceVert(backgroundImage,spliceX,newW);
}
else if (useScalingOption == SCALE_SPLICE_V_THEN_H) {
backgroundImage = ImageKit.enlargeImageSpliceVert(backgroundImage,spliceX,newW);
backgroundImage = ImageKit.enlargeImageSpliceHoriz(backgroundImage,spliceY,newH);
}
else {
backgroundImage = backgroundImage.getScaledInstance(newW,newH,useScalingOption);
}
imageW = newW;
imageH = newH;
}
}
x = (autoCenterAtConstruction || inX == Integer.MAX_VALUE) ? imageW / 2 : inX;
y = (autoCenterAtConstruction || inY == Integer.MAX_VALUE) ? imageH / 2 : inY;
}
/** Called when the image may need to be painted */
private Image paintImage() {
ImageKit.waitForImage(backgroundImage);
int imageW = backgroundImage.getWidth(ImageKit.imageObserver);
int imageH = backgroundImage.getHeight(ImageKit.imageObserver);
BufferedImage bufImage = new BufferedImage(imageW, imageH, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bufImage.createGraphics();
if (bestQuality) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
} else {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
}
/// draw the background image on to the graphics area!
g.drawImage(backgroundImage, 0, 0, imageW, imageH, ImageKit.imageObserver);
// debug our drawing surface
//g.setColor(java.awt.Color.ORANGE);
//g.drawRect(0, 0, imageW-1, imageH-1);
// call the protected paintText method to allow
// others to do the text painting
paintText(g, x, y, attributedString);
// debug - draw our preferred text boundaries
//g.setColor(java.awt.Color.green);
//g.draw(getPreferredBoundsRect(imageW,imageH));
g.dispose();
return bufImage;
}
private Rectangle2D paintText(Graphics2D g, int x, int y, AttributedString attributedString) {
if (attributedString == null)
return new Rectangle();
ACI cit = new ACI(attributedString.getIterator());
int start = cit.getBeginIndex();
int limit = cit.getEndIndex();
if (start == limit)
return new Rectangle();
FontRenderContext frc = g.getFontRenderContext();
// insert our replacement images
replaceAttributedImages(g, frc, attributedString);
TextLayout[] textLayouts = getTextLayouts(g,attributedString);
Dimension textDimensions = getTextDimensions(textLayouts);
float textHeight = (float) textDimensions.getHeight();
float textWidth = (float) textDimensions.getWidth();
if (textWidth <= 0 || textHeight <= 0)
return new Rectangle();
float fx = x;
float fy = y;
float fydraw = fy;
// debug - help us visually to see the {x,y} position
//_paintCrossHairs(g, fx, fy, java.awt.Color.RED);
//
// if we are centered vertically, then we move up to
// 1/2 the height of the total logical text box.
//
if (verticalAlignment == Alignment.CENTER)
fy = fy - (textHeight / 2.0f);
Rectangle2D textBounds = null;
for (int i = 0; i < textLayouts.length; i++) {
fydraw = fy;
//
// again if we are centered vertically, we need to move
// down 1/2 the height of the single text box
if (verticalAlignment == Alignment.CENTER)
fydraw += calcTextLayoutHeight(textLayouts[i]) / 2.0;
Rectangle2D singleTextBounds = paintRotatedTextCentered(g, textLayouts[i], fx, fydraw, textAngle, horizontalAlignment, verticalAlignment);
if (textBounds == null)
textBounds = singleTextBounds;
else
textBounds = textBounds.createUnion(singleTextBounds);
fy += calcTextLayoutHeight(textLayouts[i]);
}
// debug - help us visually to see the text boundaries
//g.setColor(java.awt.Color.orange);
//g.draw(textBounds);
return textBounds;
}
};
/**
* An AttributedCharacterIteraror delegate. Used to make debugging easier
* in Eclipse!
*/
private class ACI implements Serializable {
AttributedCharacterIterator cit;
private ACI(AttributedCharacterIterator cit) {
this.cit = cit;
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
StringBuffer buf = new StringBuffer();
for (char c = cit.first(); c != CharacterIterator.DONE; c = cit.next()) {
buf.append(c);
}
return buf.toString();
}
public AttributedCharacterIterator getIterator() {
return cit;
}
protected Object clone() throws CloneNotSupportedException {
return cit.clone();
}
public char current() {
return cit.current();
}
public boolean equals(Object obj) {
return cit.equals(obj);
}
public char first() {
return cit.first();
}
public int getBeginIndex() {
return cit.getBeginIndex();
}
public int getEndIndex() {
return cit.getEndIndex();
}
public int getIndex() {
return cit.getIndex();
}
public char last() {
return cit.last();
}
public char next() {
return cit.next();
}
public char previous() {
return cit.previous();
}
public char setIndex(int position) {
return cit.setIndex(position);
}
}
}