Path: blob/master/SLICK_HOME/src/org/newdawn/slick/AngelCodeFont.java
1456 views
package org.newdawn.slick;12import java.io.BufferedReader;3import java.io.IOException;4import java.io.InputStream;5import java.io.InputStreamReader;6import java.util.ArrayList;7import java.util.HashMap;8import java.util.Iterator;9import java.util.LinkedHashMap;10import java.util.List;11import java.util.Map;12import java.util.StringTokenizer;13import java.util.Map.Entry;1415import org.newdawn.slick.opengl.renderer.Renderer;16import org.newdawn.slick.opengl.renderer.SGL;17import org.newdawn.slick.util.Log;18import org.newdawn.slick.util.ResourceLoader;1920/**21* A font implementation that will parse BMFont format font files. The font files can be output22* by Hiero, which is included with Slick, and also the AngelCode font tool available at:23*24* <a25* href="http://www.angelcode.com/products/bmfont/">http://www.angelcode.com/products/bmfont/</a>26*27* This implementation copes with both the font display and kerning information28* allowing nicer looking paragraphs of text. Note that this utility only29* supports the text BMFont format definition file.30*31* @author kevin32* @author Nathan Sweet <[email protected]>33*/34public class AngelCodeFont implements Font {35/** The renderer to use for all GL operations */36private static SGL GL = Renderer.get();3738/**39* The line cache size, this is how many lines we can render before starting40* to regenerate lists41*/42private static final int DISPLAY_LIST_CACHE_SIZE = 200;4344/** The highest character that AngelCodeFont will support. */45private static final int MAX_CHAR = 255;4647/** True if this font should use display list caching */48private boolean displayListCaching = true;4950/** The image containing the bitmap font */51private Image fontImage;52/** The characters building up the font */53private CharDef[] chars;54/** The height of a line */55private int lineHeight;56/** The first display list ID */57private int baseDisplayListID = -1;58/** The eldest display list ID */59private int eldestDisplayListID;60/** The eldest display list */61private DisplayList eldestDisplayList;6263/** The display list cache for rendered lines */64private final LinkedHashMap displayLists = new LinkedHashMap(DISPLAY_LIST_CACHE_SIZE, 1, true) {65protected boolean removeEldestEntry(Entry eldest) {66eldestDisplayList = (DisplayList)eldest.getValue();67eldestDisplayListID = eldestDisplayList.id;6869return false;70}71};727374/**75* Create a new font based on a font definition from AngelCode's tool and76* the font image generated from the tool.77*78* @param fntFile79* The location of the font defnition file80* @param image81* The image to use for the font82* @throws SlickException83* Indicates a failure to load either file84*/85public AngelCodeFont(String fntFile, Image image) throws SlickException {86fontImage = image;8788parseFnt(ResourceLoader.getResourceAsStream(fntFile));89}9091/**92* Create a new font based on a font definition from AngelCode's tool and93* the font image generated from the tool.94*95* @param fntFile96* The location of the font defnition file97* @param imgFile98* The location of the font image99* @throws SlickException100* Indicates a failure to load either file101*/102public AngelCodeFont(String fntFile, String imgFile) throws SlickException {103fontImage = new Image(imgFile);104105parseFnt(ResourceLoader.getResourceAsStream(fntFile));106}107108/**109* Create a new font based on a font definition from AngelCode's tool and110* the font image generated from the tool.111*112* @param fntFile113* The location of the font defnition file114* @param image115* The image to use for the font116* @param caching117* True if this font should use display list caching118* @throws SlickException119* Indicates a failure to load either file120*/121public AngelCodeFont(String fntFile, Image image, boolean caching)122throws SlickException {123fontImage = image;124displayListCaching = caching;125parseFnt(ResourceLoader.getResourceAsStream(fntFile));126}127128/**129* Create a new font based on a font definition from AngelCode's tool and130* the font image generated from the tool.131*132* @param fntFile133* The location of the font defnition file134* @param imgFile135* The location of the font image136* @param caching137* True if this font should use display list caching138* @throws SlickException139* Indicates a failure to load either file140*/141public AngelCodeFont(String fntFile, String imgFile, boolean caching)142throws SlickException {143fontImage = new Image(imgFile);144displayListCaching = caching;145parseFnt(ResourceLoader.getResourceAsStream(fntFile));146}147148/**149* Create a new font based on a font definition from AngelCode's tool and150* the font image generated from the tool.151*152* @param name153* The name to assign to the font image in the image store154* @param fntFile155* The stream of the font defnition file156* @param imgFile157* The stream of the font image158* @throws SlickException159* Indicates a failure to load either file160*/161public AngelCodeFont(String name, InputStream fntFile, InputStream imgFile)162throws SlickException {163fontImage = new Image(imgFile, name, false);164165parseFnt(fntFile);166}167168/**169* Create a new font based on a font definition from AngelCode's tool and170* the font image generated from the tool.171*172* @param name173* The name to assign to the font image in the image store174* @param fntFile175* The stream of the font defnition file176* @param imgFile177* The stream of the font image178* @param caching179* True if this font should use display list caching180* @throws SlickException181* Indicates a failure to load either file182*/183public AngelCodeFont(String name, InputStream fntFile, InputStream imgFile,184boolean caching) throws SlickException {185fontImage = new Image(imgFile, name, false);186187displayListCaching = caching;188parseFnt(fntFile);189}190191/**192* Parse the font definition file193*194* @param fntFile195* The stream from which the font file can be read196* @throws SlickException197*/198private void parseFnt(InputStream fntFile) throws SlickException {199if (displayListCaching) {200baseDisplayListID = GL.glGenLists(DISPLAY_LIST_CACHE_SIZE);201if (baseDisplayListID == 0) displayListCaching = false;202}203204try {205// now parse the font file206BufferedReader in = new BufferedReader(new InputStreamReader(207fntFile));208String info = in.readLine();209String common = in.readLine();210String page = in.readLine();211212Map kerning = new HashMap(64);213List charDefs = new ArrayList(MAX_CHAR);214int maxChar = 0;215boolean done = false;216while (!done) {217String line = in.readLine();218if (line == null) {219done = true;220} else {221if (line.startsWith("chars c")) {222// ignore223} else if (line.startsWith("char")) {224CharDef def = parseChar(line);225if (def != null) {226maxChar = Math.max(maxChar, def.id);227charDefs.add(def);228}229}230if (line.startsWith("kernings c")) {231// ignore232} else if (line.startsWith("kerning")) {233StringTokenizer tokens = new StringTokenizer(line, " =");234tokens.nextToken(); // kerning235tokens.nextToken(); // first236short first = Short.parseShort(tokens.nextToken()); // first value237tokens.nextToken(); // second238int second = Integer.parseInt(tokens.nextToken()); // second value239tokens.nextToken(); // offset240int offset = Integer.parseInt(tokens.nextToken()); // offset value241List values = (List)kerning.get(new Short(first));242if (values == null) {243values = new ArrayList();244kerning.put(new Short(first), values);245}246// Pack the character and kerning offset into a short.247values.add(new Short((short)((offset << 8) | second)));248}249}250}251252chars = new CharDef[maxChar + 1];253for (Iterator iter = charDefs.iterator(); iter.hasNext();) {254CharDef def = (CharDef)iter.next();255chars[def.id] = def;256}257258// Turn each list of kerning values into a short[] and set on the chardef.259for (Iterator iter = kerning.entrySet().iterator(); iter.hasNext(); ) {260Entry entry = (Entry)iter.next();261short first = ((Short)entry.getKey()).shortValue();262List valueList = (List)entry.getValue();263short[] valueArray = new short[valueList.size()];264int i = 0;265for (Iterator valueIter = valueList.iterator(); valueIter.hasNext(); i++)266valueArray[i] = ((Short)valueIter.next()).shortValue();267chars[first].kerning = valueArray;268}269} catch (IOException e) {270Log.error(e);271throw new SlickException("Failed to parse font file: " + fntFile);272}273}274275/**276* Parse a single character line from the definition277*278* @param line279* The line to be parsed280* @return The character definition from the line281* @throws SlickException Indicates a given character is not valid in an angel code font282*/283private CharDef parseChar(String line) throws SlickException {284CharDef def = new CharDef();285StringTokenizer tokens = new StringTokenizer(line, " =");286287tokens.nextToken(); // char288tokens.nextToken(); // id289def.id = Short.parseShort(tokens.nextToken()); // id value290if (def.id < 0) {291return null;292}293if (def.id > MAX_CHAR) {294throw new SlickException("Invalid character '" + def.id295+ "': AngelCodeFont does not support characters above " + MAX_CHAR);296}297298tokens.nextToken(); // x299def.x = Short.parseShort(tokens.nextToken()); // x value300tokens.nextToken(); // y301def.y = Short.parseShort(tokens.nextToken()); // y value302tokens.nextToken(); // width303def.width = Short.parseShort(tokens.nextToken()); // width value304tokens.nextToken(); // height305def.height = Short.parseShort(tokens.nextToken()); // height value306tokens.nextToken(); // x offset307def.xoffset = Short.parseShort(tokens.nextToken()); // xoffset value308tokens.nextToken(); // y offset309def.yoffset = Short.parseShort(tokens.nextToken()); // yoffset value310tokens.nextToken(); // xadvance311def.xadvance = Short.parseShort(tokens.nextToken()); // xadvance312313def.init();314315if (def.id != ' ') {316lineHeight = Math.max(def.height + def.yoffset, lineHeight);317}318319return def;320}321322/**323* @see org.newdawn.slick.Font#drawString(float, float, java.lang.String)324*/325public void drawString(float x, float y, String text) {326drawString(x, y, text, Color.white);327}328329/**330* @see org.newdawn.slick.Font#drawString(float, float, java.lang.String,331* org.newdawn.slick.Color)332*/333public void drawString(float x, float y, String text, Color col) {334drawString(x, y, text, col, 0, text.length() - 1);335}336337/**338* @see Font#drawString(float, float, String, Color, int, int)339*/340public void drawString(float x, float y, String text, Color col,341int startIndex, int endIndex) {342fontImage.bind();343col.bind();344345GL.glTranslatef(x, y, 0);346if (displayListCaching && startIndex == 0 && endIndex == text.length() - 1) {347DisplayList displayList = (DisplayList)displayLists.get(text);348if (displayList != null) {349GL.glCallList(displayList.id);350} else {351// Compile a new display list.352displayList = new DisplayList();353displayList.text = text;354int displayListCount = displayLists.size();355if (displayListCount < DISPLAY_LIST_CACHE_SIZE) {356displayList.id = baseDisplayListID + displayListCount;357} else {358displayList.id = eldestDisplayListID;359displayLists.remove(eldestDisplayList.text);360}361362displayLists.put(text, displayList);363364GL.glNewList(displayList.id, SGL.GL_COMPILE_AND_EXECUTE);365render(text, startIndex, endIndex);366GL.glEndList();367}368} else {369render(text, startIndex, endIndex);370}371GL.glTranslatef(-x, -y, 0);372}373374/**375* Render based on immediate rendering376*377* @param text The text to be rendered378* @param start The index of the first character in the string to render379* @param end The index of the last character in the string to render380*/381private void render(String text, int start, int end) {382GL.glBegin(SGL.GL_QUADS);383384int x = 0, y = 0;385CharDef lastCharDef = null;386char[] data = text.toCharArray();387for (int i = 0; i < data.length; i++) {388int id = data[i];389if (id == '\n') {390x = 0;391y += getLineHeight();392continue;393}394if (id >= chars.length) {395continue;396}397CharDef charDef = chars[id];398if (charDef == null) {399continue;400}401402if (lastCharDef != null) x += lastCharDef.getKerning(id);403lastCharDef = charDef;404405if ((i >= start) && (i <= end)) {406charDef.draw(x, y);407}408409x += charDef.xadvance;410}411GL.glEnd();412}413414/**415* Returns the distance from the y drawing location to the top most pixel of the specified text.416*417* @param text418* The text that is to be tested419* @return The yoffset from the y draw location at which text will start420*/421public int getYOffset(String text) {422DisplayList displayList = null;423if (displayListCaching) {424displayList = (DisplayList)displayLists.get(text);425if (displayList != null && displayList.yOffset != null) return displayList.yOffset.intValue();426}427428int stopIndex = text.indexOf('\n');429if (stopIndex == -1) stopIndex = text.length();430431int minYOffset = 10000;432for (int i = 0; i < stopIndex; i++) {433int id = text.charAt(i);434CharDef charDef = chars[id];435if (charDef == null) {436continue;437}438minYOffset = Math.min(charDef.yoffset, minYOffset);439}440441if (displayList != null) displayList.yOffset = new Short((short)minYOffset);442443return minYOffset;444}445446/**447* @see org.newdawn.slick.Font#getHeight(java.lang.String)448*/449public int getHeight(String text) {450DisplayList displayList = null;451if (displayListCaching) {452displayList = (DisplayList)displayLists.get(text);453if (displayList != null && displayList.height != null) return displayList.height.intValue();454}455456int lines = 0;457int maxHeight = 0;458for (int i = 0; i < text.length(); i++) {459int id = text.charAt(i);460if (id == '\n') {461lines++;462maxHeight = 0;463continue;464}465// ignore space, it doesn't contribute to height466if (id == ' ') {467continue;468}469CharDef charDef = chars[id];470if (charDef == null) {471continue;472}473474maxHeight = Math.max(charDef.height + charDef.yoffset,475maxHeight);476}477478maxHeight += lines * getLineHeight();479480if (displayList != null) displayList.height = new Short((short)maxHeight);481482return maxHeight;483}484485/**486* @see org.newdawn.slick.Font#getWidth(java.lang.String)487*/488public int getWidth(String text) {489DisplayList displayList = null;490if (displayListCaching) {491displayList = (DisplayList)displayLists.get(text);492if (displayList != null && displayList.width != null) return displayList.width.intValue();493}494495int maxWidth = 0;496int width = 0;497CharDef lastCharDef = null;498for (int i = 0, n = text.length(); i < n; i++) {499int id = text.charAt(i);500if (id == '\n') {501width = 0;502continue;503}504if (id >= chars.length) {505continue;506}507CharDef charDef = chars[id];508if (charDef == null) {509continue;510}511512if (lastCharDef != null) width += lastCharDef.getKerning(id);513lastCharDef = charDef;514515if (i < n - 1) {516width += charDef.xadvance;517} else {518width += charDef.width;519}520maxWidth = Math.max(maxWidth, width);521}522523if (displayList != null) displayList.width = new Short((short)maxWidth);524525return maxWidth;526}527528/**529* The definition of a single character as defined in the AngelCode file530* format531*532* @author kevin533*/534private class CharDef {535/** The id of the character */536public short id;537/** The x location on the sprite sheet */538public short x;539/** The y location on the sprite sheet */540public short y;541/** The width of the character image */542public short width;543/** The height of the character image */544public short height;545/** The amount the x position should be offset when drawing the image */546public short xoffset;547/** The amount the y position should be offset when drawing the image */548public short yoffset;549550/** The amount to move the current position after drawing the character */551public short xadvance;552/** The image containing the character */553public Image image;554/** The display list index for this character */555public short dlIndex;556/** The kerning info for this character */557public short[] kerning;558559/**560* Initialise the image by cutting the right section from the map561* produced by the AngelCode tool.562*/563public void init() {564image = fontImage.getSubImage(x, y, width, height);565}566567/**568* @see java.lang.Object#toString()569*/570public String toString() {571return "[CharDef id=" + id + " x=" + x + " y=" + y + "]";572}573574/**575* Draw this character embedded in a image draw576*577* @param x578* The x position at which to draw the text579* @param y580* The y position at which to draw the text581*/582public void draw(float x, float y) {583image.drawEmbedded(x + xoffset, y + yoffset, width, height);584}585586/**587* Get the kerning offset between this character and the specified character.588* @param otherCodePoint The other code point589* @return the kerning offset590*/591public int getKerning (int otherCodePoint) {592if (kerning == null) return 0;593int low = 0;594int high = kerning.length - 1;595while (low <= high) {596int midIndex = (low + high) >>> 1;597int value = kerning[midIndex];598int foundCodePoint = value & 0xff;599if (foundCodePoint < otherCodePoint)600low = midIndex + 1;601else if (foundCodePoint > otherCodePoint)602high = midIndex - 1;603else604return value >> 8;605}606return 0;607}608}609610/**611* @see org.newdawn.slick.Font#getLineHeight()612*/613public int getLineHeight() {614return lineHeight;615}616617/**618* A descriptor for a single display list619*620* @author Nathan Sweet <[email protected]>621*/622static private class DisplayList {623/** The if of the distance list */624int id;625/** The offset of the line rendered */626Short yOffset;627/** The width of the line rendered */628Short width;629/** The height of the line rendered */630Short height;631/** The text that the display list holds */632String text;633}634}635636637