package org.newdawn.slick;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.StringTokenizer;
import org.newdawn.slick.opengl.renderer.Renderer;
import org.newdawn.slick.opengl.renderer.SGL;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
/**
* A font implementation that will parse BMFont format font files. The font files can be output
* by Hiero, which is included with Slick, and also the AngelCode font tool available at:
*
* <a
* href="http://www.angelcode.com/products/bmfont/">http://www.angelcode.com/products/bmfont/</a>
*
* This implementation copes with both the font display and kerning information
* allowing nicer looking paragraphs of text. Note that this utility only
* supports the text BMFont format definition file.
*
* @author kevin
* @author Nathan Sweet <misc@n4te.com>
*/
public class AngelCodeFont implements Font {
/** The renderer to use for all GL operations */
private static SGL GL = Renderer.get();
/**
* The line cache size, this is how many lines we can render before starting
* to regenerate lists
*/
private static final int DISPLAY_LIST_CACHE_SIZE = 200;
/** The highest character that AngelCodeFont will support. */
private static final int MAX_CHAR = 255;
/** True if this font should use display list caching */
private boolean displayListCaching = true;
/** The image containing the bitmap font */
private Image fontImage;
/** The characters building up the font */
private Glyph[] chars;
/** The height of a line */
private int lineHeight;
/** The first display list ID */
private int baseDisplayListID = -1;
/** The eldest display list ID */
private int eldestDisplayListID;
/** The eldest display list */
private DisplayList eldestDisplayList;
private boolean singleCase = false;
private short ascent;
private short descent;
private short leading; //TODO: fix leading, use for multi-line text
/** The display list cache for rendered lines */
private final LinkedHashMap displayLists = new LinkedHashMap(DISPLAY_LIST_CACHE_SIZE, 1, true) {
protected boolean removeEldestEntry(Entry eldest) {
eldestDisplayList = (DisplayList)eldest.getValue();
eldestDisplayListID = eldestDisplayList.id;
return false;
}
};
/**
* Create a new font based on a font definition from AngelCode's tool and
* the font image generated from the tool.
*
* @param fntFile
* The location of the font defnition file
* @param image
* The image to use for the font
* @throws SlickException
* Indicates a failure to load either file
*/
public AngelCodeFont(String fntFile, Image image) throws SlickException {
fontImage = image;
parseFnt(ResourceLoader.getResourceAsStream(fntFile));
}
/**
* Create a new font based on a font definition from AngelCode's tool and
* the font image generated from the tool.
*
* @param fntFile
* The location of the font defnition file
* @param imgFile
* The location of the font image
* @throws SlickException
* Indicates a failure to load either file
*/
public AngelCodeFont(String fntFile, String imgFile) throws SlickException {
fontImage = new Image(imgFile);
parseFnt(ResourceLoader.getResourceAsStream(fntFile));
}
/**
* Create a new font based on a font definition from AngelCode's tool and
* the font image generated from the tool.
*
* @param fntFile
* The location of the font defnition file
* @param image
* The image to use for the font
* @param caching
* True if this font should use display list caching
* @throws SlickException
* Indicates a failure to load either file
*/
public AngelCodeFont(String fntFile, Image image, boolean caching)
throws SlickException {
fontImage = image;
displayListCaching = caching;
parseFnt(ResourceLoader.getResourceAsStream(fntFile));
}
/**
* Create a new font based on a font definition from AngelCode's tool and
* the font image generated from the tool.
*
* @param fntFile
* The location of the font defnition file
* @param imgFile
* The location of the font image
* @param caching
* True if this font should use display list caching
* @throws SlickException
* Indicates a failure to load either file
*/
public AngelCodeFont(String fntFile, String imgFile, boolean caching)
throws SlickException {
fontImage = new Image(imgFile);
displayListCaching = caching;
parseFnt(ResourceLoader.getResourceAsStream(fntFile));
}
/**
* Create a new font based on a font definition from AngelCode's tool and
* the font image generated from the tool.
*
* @param name
* The name to assign to the font image in the image store
* @param fntFile
* The stream of the font defnition file
* @param imgFile
* The stream of the font image
* @throws SlickException
* Indicates a failure to load either file
*/
public AngelCodeFont(String name, InputStream fntFile, InputStream imgFile)
throws SlickException {
fontImage = new Image(imgFile, name, false);
parseFnt(fntFile);
}
/**
* Create a new font based on a font definition from AngelCode's tool and
* the font image generated from the tool.
*
* @param name
* The name to assign to the font image in the image store
* @param fntFile
* The stream of the font defnition file
* @param imgFile
* The stream of the font image
* @param caching
* True if this font should use display list caching
* @throws SlickException
* Indicates a failure to load either file
*/
public AngelCodeFont(String name, InputStream fntFile, InputStream imgFile,
boolean caching) throws SlickException {
fontImage = new Image(imgFile, name, false);
displayListCaching = caching;
parseFnt(fntFile);
}
/**
* Parse the font definition file
*
* @param fntFile
* The stream from which the font file can be read
* @throws SlickException
*/
private void parseFnt(InputStream fntFile) throws SlickException {
if (displayListCaching) {
baseDisplayListID = GL.glGenLists(DISPLAY_LIST_CACHE_SIZE);
if (baseDisplayListID == 0) displayListCaching = false;
}
try {
// now parse the font file
BufferedReader in = new BufferedReader(new InputStreamReader(
fntFile));
String info = in.readLine();
String common = in.readLine();
ascent = parseMetric(common, "base="); //not used apparently ?
//ascent = parseMetric(common, "ascent=");
descent = parseMetric(common, "descent=");
leading = parseMetric(common, "leading=");
String page = in.readLine();
Map<Short, List<Short>> kerning = new HashMap<Short, List<Short>>(64);
List<Glyph> charDefs = new ArrayList<Glyph>(MAX_CHAR);
int maxChar = 0;
boolean done = false;
while (!done) {
String line = in.readLine();
if (line == null) {
done = true;
} else {
if (line.startsWith("chars c")) {
// ignore
} else if (line.startsWith("char")) {
Glyph def = parseChar(line);
if (def != null) {
maxChar = Math.max(maxChar, def.id);
charDefs.add(def);
}
}
if (line.startsWith("kernings c")) {
// ignore
} else if (line.startsWith("kerning")) {
StringTokenizer tokens = new StringTokenizer(line, " =");
tokens.nextToken(); // kerning
tokens.nextToken(); // first
short first = Short.parseShort(tokens.nextToken()); // first
// value
tokens.nextToken(); // second
int second = Integer.parseInt(tokens.nextToken()); // second
// value
tokens.nextToken(); // offset
int offset = Integer.parseInt(tokens.nextToken()); // offset
// value
List<Short> values = kerning.get(first);
if (values == null) {
values = new ArrayList<Short>();
kerning.put(first, values);
}
// Pack the character and kerning offset into a short.
values.add((short) ((offset << 8) | second));
}
}
}
chars = new Glyph[maxChar + 1];
for (Glyph def : charDefs) {
chars[def.id] = def;
}
// Turn each list of kerning values into a short[] and set on the
// chardef.
for (Entry<Short, List<Short>> entry : kerning.entrySet()) {
short first = entry.getKey();
List<Short> valueList = entry.getValue();
short[] valueArray = new short[valueList.size()];
for (int i=0; i<valueList.size(); i++)
valueArray[i] = valueList.get(i);
chars[first].kerning = valueArray;
}
} catch (IOException e) {
Log.error(e);
throw new SlickException("Failed to parse font file: " + fntFile);
}
}
/**
* Returns the sprite sheet image that holds all of the images.
* @return the image for this bitmap font
*/
public Image getImage() {
return fontImage;
}
/**
* If a font has the same glyphs for upper and lower case text, we can
* minimize its glyph page size by only using one case. If single case
* is enabled (by default it is disabled), then the getGlyph method
* (and, in turn, glyph rendering/height/width/etc.) will try to find
* whichever case exists for the given code point (between 65-90 for
* upper case characters and 97-122 for lower case).
*
* @param enabled true to enable
*/
public void setSingleCase(boolean enabled) {
this.singleCase = enabled;
}
/**
* If a font has the same glyphs for upper and lower case text, we can
* minimize its glyph page size by only using one case. If single case
* is enabled (by default it is disabled), then the getGlyph method
* (and, in turn, glyph rendering/height/width/etc.) will try to find
* whichever case exists for the given code point (between 65-90 for
* upper case characters and 97-122 for lower case).
*
* @return true if single case is enabled
*/
public boolean isSingleCase() {
return singleCase;
}
private short parseMetric(String str, String sub) {
int ind = str.indexOf(sub);
if (ind!=-1) {
String subStr = str.substring(ind+sub.length());
ind = subStr.indexOf(' ');
return Short.parseShort(subStr.substring(0, ind!=-1 ? ind : subStr.length()));
}
return -1;
}
/**
* Parse a single character line from the definition
*
* @param line
* The line to be parsed
* @return The character definition from the line
* @throws SlickException Indicates a given character is not valid in an angel code font
*/
private Glyph parseChar(String line) throws SlickException {
StringTokenizer tokens = new StringTokenizer(line, " =");
tokens.nextToken(); // char
tokens.nextToken(); // id
short id = Short.parseShort(tokens.nextToken()); // id value
if (id < 0) {
return null;
}
if (id > MAX_CHAR) {
throw new SlickException("Invalid character '" + id
+ "': SpriteFont does not support characters above "
+ MAX_CHAR);
}
tokens.nextToken(); // x
short x = Short.parseShort(tokens.nextToken()); // x value
tokens.nextToken(); // y
short y = Short.parseShort(tokens.nextToken()); // y value
tokens.nextToken(); // width
short width = Short.parseShort(tokens.nextToken()); // width value
tokens.nextToken(); // height
short height = Short.parseShort(tokens.nextToken()); // height value
tokens.nextToken(); // x offset
short xoffset = Short.parseShort(tokens.nextToken()); // xoffset value
tokens.nextToken(); // y offset
short yoffset = Short.parseShort(tokens.nextToken()); // yoffset value
tokens.nextToken(); // xadvance
short xadvance = Short.parseShort(tokens.nextToken()); // xadvance
if (id != ' ') {
lineHeight = Math.max(height + yoffset, lineHeight);
}
Image img = fontImage.getSubImage(x, y, width, height);
return new Glyph(id, x, y, width, height, xoffset, yoffset, xadvance, img);
}
/**
* @see org.newdawn.slick.Font#drawString(float, float, java.lang.String)
*/
public void drawString(float x, float y, CharSequence text) {
drawString(x, y, text, Color.white);
}
/**
* @see org.newdawn.slick.Font#drawString(float, float, java.lang.String,
* org.newdawn.slick.Color)
*/
public void drawString(float x, float y, CharSequence text, Color col) {
drawString(x, y, text, col, 0, text.length() - 1);
}
/**
* @see Font#drawString(float, float, String, Color, int, int)
*/
public void drawString(float x, float y, CharSequence text, Color col,
int startIndex, int endIndex) {
fontImage.bind();
col.bind();
GL.glTranslatef(x, y, 0);
if (displayListCaching && startIndex == 0 && endIndex == text.length() - 1) {
DisplayList displayList = (DisplayList)displayLists.get(text);
if (displayList != null) {
GL.glCallList(displayList.id);
} else {
// Compile a new display list.
displayList = new DisplayList();
displayList.text = text;
int displayListCount = displayLists.size();
if (displayListCount < DISPLAY_LIST_CACHE_SIZE) {
displayList.id = baseDisplayListID + displayListCount;
} else {
displayList.id = eldestDisplayListID;
displayLists.remove(eldestDisplayList.text);
}
displayLists.put(text, displayList);
GL.glNewList(displayList.id, SGL.GL_COMPILE_AND_EXECUTE);
render(text, startIndex, endIndex);
GL.glEndList();
}
} else {
render(text, startIndex, endIndex);
}
GL.glTranslatef(-x, -y, 0);
}
/**
* Render based on immediate rendering
*
* @param text The text to be rendered
* @param start The index of the first character in the string to render
* @param end The index of the last character in the string to render
*/
private void render(CharSequence text, int start, int end) {
GL.glBegin(SGL.GL_QUADS);
int x = 0, y = 0;
Glyph lastCharDef = null;
for (int i = 0; i < text.length(); i++) {
char id = text.charAt(i);
if (id == '\n') {
x = 0;
y += getLineHeight();
continue;
}
Glyph charDef = getGlyph(id);
if (charDef == null) {
continue;
}
if (lastCharDef != null)
x += lastCharDef.getKerning(id);
else
x -= charDef.xoffset;
lastCharDef = charDef;
if ((i >= start) && (i <= end)) {
charDef.image.drawEmbedded(x + charDef.xoffset, y + charDef.yoffset, charDef.width, charDef.height);
}
x += charDef.xadvance;
}
GL.glEnd();
}
/**
* Returns the distance from the y drawing location to the top most pixel of the specified text.
*
* @param text
* The text that is to be tested
* @return The yoffset from the y draw location at which text will start
*/
public int getYOffset(String text) {
DisplayList displayList = null;
if (displayListCaching) {
displayList = (DisplayList)displayLists.get(text);
if (displayList != null && displayList.yOffset != null) return displayList.yOffset.intValue();
}
int stopIndex = text.indexOf('\n');
if (stopIndex == -1) stopIndex = text.length();
int minYOffset = 10000;
for (int i = 0; i < stopIndex; i++) {
Glyph charDef = getGlyph(text.charAt(i));
if (charDef == null) {
continue;
}
minYOffset = Math.min(charDef.yoffset, minYOffset);
}
if (displayList != null) displayList.yOffset = new Short((short)minYOffset);
return minYOffset;
}
/**
* @see org.newdawn.slick.Font#getHeight(java.lang.String)
*/
public int getHeight(CharSequence text) {
DisplayList displayList = null;
if (displayListCaching) {
displayList = (DisplayList)displayLists.get(text);
if (displayList != null && displayList.height != null) return displayList.height.intValue();
}
int lines = 0;
int maxHeight = 0;
for (int i = 0; i < text.length(); i++) {
char id = text.charAt(i);
if (id == '\n') {
lines++;
maxHeight = 0;
continue;
}
// ignore space, it doesn't contribute to height
if (id == ' ') {
continue;
}
Glyph charDef = getGlyph(id);
if (charDef == null) {
continue;
}
maxHeight = Math.max(charDef.height + charDef.yoffset,
maxHeight);
}
maxHeight += lines * getLineHeight();
if (displayList != null) displayList.height = new Short((short)maxHeight);
return maxHeight;
}
/**
* @see org.newdawn.slick.Font#getWidth(java.lang.String)
*/
public int getWidth(CharSequence text) {
DisplayList displayList = null;
if (displayListCaching) {
displayList = (DisplayList)displayLists.get(text);
if (displayList != null && displayList.width != null) return displayList.width.intValue();
}
int maxWidth = 0;
int width = 0;
Glyph lastCharDef = null;
for (int i = 0, n = text.length(); i < n; i++) {
char id = text.charAt(i);
if (id == '\n') {
width = 0;
continue;
}
Glyph charDef = getGlyph(id);
if (charDef == null) {
continue;
}
if (lastCharDef != null)
width += lastCharDef.getKerning(id);
// else //first glyph
// width -= charDef.xoffset;
//
lastCharDef = charDef;
//space characters have zero width, so use their xadvance instead
if (i < n - 1 || charDef.width==0) {
width += charDef.xadvance;
} else {
width += charDef.width + charDef.xoffset;
}
maxWidth = Math.max(maxWidth, width);
}
if (displayList != null) displayList.width = new Short((short)maxWidth);
return maxWidth;
}
/**
* @see org.newdawn.slick.Font#getLineHeight()
*/
public int getLineHeight() {
return lineHeight;
}
/**
* Requires export from the newest version of Hiero. Alternatively, you
* could manually add <tt>descent=XX</tt> to the end of the 'common' line
* in your font file.
*
* The descent is the distance from the font's baseline to the
* bottom of most alphanumeric characters with descenders.
*/
public int getDescent() {
return descent;
}
/**
* Requires export from the newest version of Hiero. Alternatively, you
* could manually add <tt>ascent=XX</tt> to the end of the 'common' line
* in your font file.
*
* The ascent is the distance from the font's baseline to the top of most
* alphanumeric characters.
*/
public int getAscent() {
return ascent;
}
/**
* Returns the character definition for the given character.
*
* @param c the desired character
* @return the CharDef with glyph info
*/
public Glyph getGlyph(char c) {
Glyph g = c<0 || c>= chars.length ? null : chars[c];
if (g!=null)
return g;
if (g==null && singleCase) {
if (c>=65 && c<=90)
c += 32;
else if (c>=97 && c<=122)
c -= 32;
}
return c<0 || c>= chars.length ? null : chars[c];
}
/**
* The definition of a single character as defined in the AngelCode file
* format
*
* @author kevin
*/
public static class Glyph {
/** The id of the character */
public final short id;
/** The x location on the sprite sheet */
public final short x;
/** The y location on the sprite sheet */
public final short y;
/** The width of the character image */
public final short width;
/** The height of the character image */
public final short height;
/** The amount the x position should be offset when drawing the image */
public final short xoffset;
/** The amount the y position should be offset when drawing the image */
public final short yoffset;
/** The amount to move the current position after drawing the character */
public final short xadvance;
/** The sub-image containing the character */
public final Image image;
/** The display list index for this character */
protected short dlIndex;
/** The kerning info for this character */
protected short[] kerning;
protected Glyph(short id, short x, short y, short width, short height,
short xoffset, short yoffset, short xadvance, Image image) {
this.id = id;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.xoffset = xoffset;
this.yoffset = yoffset;
this.xadvance = xadvance;
this.image = image;
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
return "[CharDef id=" + id + " x=" + x + " y=" + y + "]";
}
/**
* Get the kerning offset between this character and the specified character.
* @param otherCodePoint The other code point
* @return the kerning offset
*/
public int getKerning (int otherCodePoint) {
if (kerning == null) return 0;
int low = 0;
int high = kerning.length - 1;
while (low <= high) {
int midIndex = (low + high) >>> 1;
int value = kerning[midIndex];
int foundCodePoint = value & 0xff;
if (foundCodePoint < otherCodePoint)
low = midIndex + 1;
else if (foundCodePoint > otherCodePoint)
high = midIndex - 1;
else
return value >> 8;
}
return 0;
}
}
/**
* A descriptor for a single display list
*
* @author Nathan Sweet <misc@n4te.com>
*/
static private class DisplayList {
/** The if of the distance list */
int id;
/** The offset of the line rendered */
Short yOffset;
/** The width of the line rendered */
Short width;
/** The height of the line rendered */
Short height;
/** The text that the display list holds */
CharSequence text;
}
}