Java tutorial
/* * Copyright (c) 2008-2010, Matthias Mann * * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following * conditions are met: * * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Matthias Mann nor * the names of its contributors may be used to endorse or promote products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT * SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.badlogic.gdx.graphics.g2d; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.StringTokenizer; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.Texture.TextureFilter; import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion; import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.FloatArray; import com.badlogic.gdx.utils.GdxRuntimeException; import com.badlogic.gdx.utils.StreamUtils; /** Renders bitmap fonts. The font consists of 2 files: an image file or {@link TextureRegion} containing the glyphs and a file in * the AngleCode BMFont text format that describes where each glyph is on the image. Currently only a single image of glyphs is * supported.<br> * <br> * Text is drawn using a {@link Batch}. Text can be cached in a {@link BitmapFontCache} for faster rendering of static text, which * saves needing to compute the location of each glyph each frame.<br> * <br> * * The texture for a BitmapFont loaded from a file is managed. {@link #dispose()} must be called to free the texture when no * longer needed. A BitmapFont loaded using a {@link TextureRegion} is managed if the region's texture is managed. Disposing the * BitmapFont disposes the region's texture, which may not be desirable if the texture is still being used elsewhere.<br> * <br> * The code was originally based on Matthias Mann's TWL BitmapFont class. Thanks for sharing, Matthias! :) * @author Nathan Sweet * @author Matthias Mann */ public class BitmapFont implements Disposable { static private final int LOG2_PAGE_SIZE = 9; static private final int PAGE_SIZE = 1 << LOG2_PAGE_SIZE; static private final int PAGES = 0x10000 / PAGE_SIZE; public static final char[] xChars = { 'x', 'e', 'a', 'o', 'n', 's', 'r', 'c', 'u', 'm', 'v', 'w', 'z' }; public static final char[] capChars = { 'M', 'N', 'B', 'D', 'C', 'E', 'F', 'K', 'A', 'G', 'H', 'I', 'J', 'L', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' }; final BitmapFontData data; TextureRegion[] regions; private final BitmapFontCache cache; private boolean flipped; private boolean integer; private boolean ownsTexture; boolean markupEnabled; /** Creates a BitmapFont using the default 15pt Arial font included in the libgdx JAR file. This is convenient to easily display * text without bothering with generating a bitmap font. */ public BitmapFont() { this(Gdx.files.classpath("com/badlogic/gdx/utils/arial-15.fnt"), Gdx.files.classpath("com/badlogic/gdx/utils/arial-15.png"), false, true); } /** Creates a BitmapFont using the default 15pt Arial font included in the libgdx JAR file. This is convenient to easily display * text without bothering with generating a bitmap font. * @param flip If true, the glyphs will be flipped for use with a perspective where 0,0 is the upper left corner. */ public BitmapFont(boolean flip) { this(Gdx.files.classpath("com/badlogic/gdx/utils/arial-15.fnt"), Gdx.files.classpath("com/badlogic/gdx/utils/arial-15.png"), flip, true); } /** Creates a BitmapFont with the glyphs relative to the specified region. If the region is null, the glyph textures are loaded * from the image file given in the font file. The {@link #dispose()} method will not dispose the region's texture in this * case! * * The font data is not flipped. * * @param fontFile the font definition file * @param region The texture region containing the glyphs. The glyphs must be relative to the lower left corner (ie, the region * should not be flipped). If the region is null the glyph images are loaded from the image path in the font file. */ public BitmapFont(FileHandle fontFile, TextureRegion region) { this(fontFile, region, false); } /** Creates a BitmapFont with the glyphs relative to the specified region. If the region is null, the glyph textures are loaded * from the image file given in the font file. The {@link #dispose()} method will not dispose the region's texture in this * case! * @param region The texture region containing the glyphs. The glyphs must be relative to the lower left corner (ie, the region * should not be flipped). If the region is null the glyph images are loaded from the image path in the font file. * @param flip If true, the glyphs will be flipped for use with a perspective where 0,0 is the upper left corner. */ public BitmapFont(FileHandle fontFile, TextureRegion region, boolean flip) { this(new BitmapFontData(fontFile, flip), region, true); } /** Creates a BitmapFont from a BMFont file. The image file name is read from the BMFont file and the image is loaded from the * same directory. The font data is not flipped. */ public BitmapFont(FileHandle fontFile) { this(fontFile, false); } /** Creates a BitmapFont from a BMFont file. The image file name is read from the BMFont file and the image is loaded from the * same directory. * @param flip If true, the glyphs will be flipped for use with a perspective where 0,0 is the upper left corner. */ public BitmapFont(FileHandle fontFile, boolean flip) { this(new BitmapFontData(fontFile, flip), (TextureRegion) null, true); } /** Creates a BitmapFont from a BMFont file, using the specified image for glyphs. Any image specified in the BMFont file is * ignored. * @param flip If true, the glyphs will be flipped for use with a perspective where 0,0 is the upper left corner. */ public BitmapFont(FileHandle fontFile, FileHandle imageFile, boolean flip) { this(fontFile, imageFile, flip, true); } /** Creates a BitmapFont from a BMFont file, using the specified image for glyphs. Any image specified in the BMFont file is * ignored. * @param flip If true, the glyphs will be flipped for use with a perspective where 0,0 is the upper left corner. * @param integer If true, rendering positions will be at integer values to avoid filtering artifacts. */ public BitmapFont(FileHandle fontFile, FileHandle imageFile, boolean flip, boolean integer) { this(new BitmapFontData(fontFile, flip), new TextureRegion(new Texture(imageFile, false)), integer); ownsTexture = true; } /** Constructs a new BitmapFont from the given {@link BitmapFontData} and {@link TextureRegion}. If the TextureRegion is null, * the image path(s) will be read from the BitmapFontData. The dispose() method will not dispose the texture of the region(s) * if the region is != null. * * Passing a single TextureRegion assumes that your font only needs a single texture page. If you need to support multiple * pages, either let the Font read the images themselves (by specifying null as the TextureRegion), or by specifying each page * manually with the TextureRegion[] constructor. * * @param integer If true, rendering positions will be at integer values to avoid filtering artifacts. */ public BitmapFont(BitmapFontData data, TextureRegion region, boolean integer) { this(data, region != null ? new TextureRegion[] { region } : null, integer); } /** Constructs a new BitmapFont from the given {@link BitmapFontData} and array of {@link TextureRegion}. If the TextureRegion * is null or empty, the image path(s) will be read from the BitmapFontData. The dispose() method will not dispose the texture * of the region(s) if the regions array is != null and not empty. * * @param integer If true, rendering positions will be at integer values to avoid filtering artifacts. */ public BitmapFont(BitmapFontData data, TextureRegion[] regions, boolean integer) { if (regions == null || regions.length == 0) { // load each path this.regions = new TextureRegion[data.imagePaths.length]; for (int i = 0; i < this.regions.length; i++) { if (data.fontFile == null) { this.regions[i] = new TextureRegion(new Texture(Gdx.files.internal(data.imagePaths[i]), false)); } else { this.regions[i] = new TextureRegion( new Texture(Gdx.files.getFileHandle(data.imagePaths[i], data.fontFile.type()), false)); } } ownsTexture = true; } else { this.regions = regions; ownsTexture = false; } cache = new BitmapFontCache(this); cache.setUseIntegerPositions(integer); this.flipped = data.flipped; this.data = data; this.integer = integer; load(data); } private void load(BitmapFontData data) { for (Glyph[] page : data.glyphs) { if (page == null) continue; for (Glyph glyph : page) { if (glyph == null) continue; TextureRegion region = regions[glyph.page]; if (region == null) { // TODO: support null regions by parsing scaleW / scaleH ? throw new IllegalArgumentException( "BitmapFont texture region array cannot contain null elements"); } float invTexWidth = 1.0f / region.getTexture().getWidth(); float invTexHeight = 1.0f / region.getTexture().getHeight(); float offsetX = 0, offsetY = 0; float u = region.u; float v = region.v; float regionWidth = region.getRegionWidth(); float regionHeight = region.getRegionHeight(); if (region instanceof AtlasRegion) { // Compensate for whitespace stripped from left and top edges. AtlasRegion atlasRegion = (AtlasRegion) region; offsetX = atlasRegion.offsetX; offsetY = atlasRegion.originalHeight - atlasRegion.packedHeight - atlasRegion.offsetY; } float x = glyph.srcX; float x2 = glyph.srcX + glyph.width; float y = glyph.srcY; float y2 = glyph.srcY + glyph.height; // Shift glyph for left and top edge stripped whitespace. Clip glyph for right and bottom edge stripped whitespace. if (offsetX > 0) { x -= offsetX; if (x < 0) { glyph.width += x; glyph.xoffset -= x; x = 0; } x2 -= offsetX; if (x2 > regionWidth) { glyph.width -= x2 - regionWidth; x2 = regionWidth; } } if (offsetY > 0) { y -= offsetY; if (y < 0) { glyph.height += y; y = 0; } y2 -= offsetY; if (y2 > regionHeight) { float amount = y2 - regionHeight; glyph.height -= amount; glyph.yoffset += amount; y2 = regionHeight; } } glyph.u = u + x * invTexWidth; glyph.u2 = u + x2 * invTexWidth; if (data.flipped) { glyph.v = v + y * invTexHeight; glyph.v2 = v + y2 * invTexHeight; } else { glyph.v2 = v + y * invTexHeight; glyph.v = v + y2 * invTexHeight; } } } } /** Draws a string at the specified position. * @see BitmapFontCache#addText(CharSequence, float, float, int, int) */ public TextBounds draw(Batch batch, CharSequence str, float x, float y) { cache.clear(); TextBounds bounds = cache.addText(str, x, y, 0, str.length()); cache.draw(batch); return bounds; } /** Draws a string at the specified position. * @see BitmapFontCache#addText(CharSequence, float, float, int, int) */ public TextBounds draw(Batch batch, CharSequence str, float x, float y, int start, int end) { cache.clear(); TextBounds bounds = cache.addText(str, x, y, start, end); cache.draw(batch); return bounds; } /** Draws a string, which may contain newlines (\n), at the specified position. * @see BitmapFontCache#addMultiLineText(CharSequence, float, float, float, HAlignment) */ public TextBounds drawMultiLine(Batch batch, CharSequence str, float x, float y) { cache.clear(); TextBounds bounds = cache.addMultiLineText(str, x, y, 0, HAlignment.LEFT); cache.draw(batch); return bounds; } /** Draws a string, which may contain newlines (\n), at the specified position. * @see BitmapFontCache#addMultiLineText(CharSequence, float, float, float, HAlignment) */ public TextBounds drawMultiLine(Batch batch, CharSequence str, float x, float y, float alignmentWidth, HAlignment alignment) { cache.clear(); TextBounds bounds = cache.addMultiLineText(str, x, y, alignmentWidth, alignment); cache.draw(batch); return bounds; } /** Draws a string, which may contain newlines (\n), with the specified position. Each line is automatically wrapped within the * specified width. * @see BitmapFontCache#addWrappedText(CharSequence, float, float, float, HAlignment) */ public TextBounds drawWrapped(Batch batch, CharSequence str, float x, float y, float wrapWidth) { cache.clear(); TextBounds bounds = cache.addWrappedText(str, x, y, wrapWidth, HAlignment.LEFT); cache.draw(batch); return bounds; } /** Draws a string, which may contain newlines (\n), with the specified position. Each line is automatically wrapped within the * specified width. * @see BitmapFontCache#addWrappedText(CharSequence, float, float, float, HAlignment) */ public TextBounds drawWrapped(Batch batch, CharSequence str, float x, float y, float wrapWidth, HAlignment alignment) { cache.clear(); TextBounds bounds = cache.addWrappedText(str, x, y, wrapWidth, alignment); cache.draw(batch); return bounds; } /** Returns the bounds of the specified text. Note the returned TextBounds instance is reused. * @see #getBounds(CharSequence, int, int, TextBounds) */ public TextBounds getBounds(CharSequence str) { return getBounds(str, 0, str.length(), cache.getBounds()); } /** Returns the bounds of the specified text. * @see #getBounds(CharSequence, int, int, TextBounds) */ public TextBounds getBounds(CharSequence str, TextBounds textBounds) { return getBounds(str, 0, str.length(), textBounds); } /** Returns the bounds of the specified text. Note the returned TextBounds instance is reused. * @see #getBounds(CharSequence, int, int, TextBounds) */ public TextBounds getBounds(CharSequence str, int start, int end) { return getBounds(str, start, end, cache.getBounds()); } /** Returns the size of the specified string. The height is the distance from the top of most capital letters in the font (the * {@link #getCapHeight() cap height}) to the baseline. * @param start The first character of the string. * @param end The last character of the string (exclusive). */ public TextBounds getBounds(CharSequence str, int start, int end, TextBounds textBounds) { BitmapFontData data = this.data; int width = 0; Glyph lastGlyph = null; while (start < end) { char ch = str.charAt(start++); if (ch == '[' && markupEnabled) { if (!(start < end && str.charAt(start) == '[')) { // non escaped '[' while (start < end && str.charAt(start) != ']') start++; start++; continue; } start++; } lastGlyph = data.getGlyph(ch); if (lastGlyph != null) { width = lastGlyph.xadvance; break; } } while (start < end) { char ch = str.charAt(start++); if (ch == '[' && markupEnabled) { if (!(start < end && str.charAt(start) == '[')) { // non escaped '[' while (start < end && str.charAt(start) != ']') start++; start++; continue; } start++; } Glyph g = data.getGlyph(ch); if (g != null) { width += lastGlyph.getKerning(ch); lastGlyph = g; width += g.xadvance; } } textBounds.width = width * data.scaleX; textBounds.height = data.capHeight; return textBounds; } /** Returns the bounds of the specified text, which may contain newlines. * @see #getMultiLineBounds(CharSequence, TextBounds) */ public TextBounds getMultiLineBounds(CharSequence str) { return getMultiLineBounds(str, cache.getBounds()); } /** Returns the bounds of the specified text, which may contain newlines. The height is the distance from the top of most * capital letters in the font (the {@link #getCapHeight() cap height}) to the baseline of the last line of text. */ public TextBounds getMultiLineBounds(CharSequence str, TextBounds textBounds) { int start = 0; float maxWidth = 0; int numLines = 0; int length = str.length(); while (start < length) { int lineEnd = indexOf(str, '\n', start); float lineWidth = getBounds(str, start, lineEnd).width; maxWidth = Math.max(maxWidth, lineWidth); start = lineEnd + 1; numLines++; } textBounds.width = maxWidth; textBounds.height = data.capHeight + (numLines - 1) * data.lineHeight; return textBounds; } /** Returns the bounds of the specified text, which may contain newlines and is wrapped within the specified width. * @see #getWrappedBounds(CharSequence, float, TextBounds) */ public TextBounds getWrappedBounds(CharSequence str, float wrapWidth) { return getWrappedBounds(str, wrapWidth, cache.getBounds()); } /** Returns the bounds of the specified text, which may contain newlines and is wrapped within the specified width. The height * is the distance from the top of most capital letters in the font (the {@link #getCapHeight() cap height}) to the baseline of * the last line of text. * @param wrapWidth Width to wrap the bounds within. */ public TextBounds getWrappedBounds(CharSequence str, float wrapWidth, TextBounds textBounds) { if (wrapWidth <= 0) wrapWidth = Integer.MAX_VALUE; int start = 0; int numLines = 0; int length = str.length(); float maxWidth = 0; while (start < length) { int newLine = BitmapFont.indexOf(str, '\n', start); // Eat whitespace at start of line. while (start < newLine) { if (!BitmapFont.isWhitespace(str.charAt(start))) break; start++; } int lineEnd = start + computeVisibleGlyphs(str, start, newLine, wrapWidth); int nextStart = lineEnd + 1; if (lineEnd < newLine) { // Find char to break on. while (lineEnd > start) { if (BitmapFont.isWhitespace(str.charAt(lineEnd))) break; lineEnd--; } if (lineEnd == start) { if (nextStart > start + 1) nextStart--; lineEnd = nextStart; // If no characters to break, show all. } else { nextStart = lineEnd; // Eat whitespace at end of line. while (lineEnd > start) { if (!BitmapFont.isWhitespace(str.charAt(lineEnd - 1))) break; lineEnd--; } } } if (lineEnd > start) { float lineWidth = getBounds(str, start, lineEnd).width; maxWidth = Math.max(maxWidth, lineWidth); } start = nextStart; numLines++; } textBounds.width = maxWidth; textBounds.height = data.capHeight + (numLines - 1) * data.lineHeight; return textBounds; } /** Computes the glyph advances for the given character sequence and stores them in the provided {@link FloatArray}. The float * arrays are cleared. An additional element is added at the end. * @param glyphAdvances the glyph advances output array. * @param glyphPositions the glyph positions output array. */ public void computeGlyphAdvancesAndPositions(CharSequence str, FloatArray glyphAdvances, FloatArray glyphPositions) { glyphAdvances.clear(); glyphPositions.clear(); int index = 0; int end = str.length(); float width = 0; Glyph lastGlyph = null; BitmapFontData data = this.data; if (data.scaleX == 1) { for (; index < end; index++) { char ch = str.charAt(index); Glyph g = data.getGlyph(ch); if (g != null) { if (lastGlyph != null) width += lastGlyph.getKerning(ch); lastGlyph = g; glyphAdvances.add(g.xadvance); glyphPositions.add(width); width += g.xadvance; } } glyphAdvances.add(0); glyphPositions.add(width); } else { float scaleX = this.data.scaleX; for (; index < end; index++) { char ch = str.charAt(index); Glyph g = data.getGlyph(ch); if (g != null) { if (lastGlyph != null) width += lastGlyph.getKerning(ch) * scaleX; lastGlyph = g; float xadvance = g.xadvance * scaleX; glyphAdvances.add(xadvance); glyphPositions.add(width); width += xadvance; } } glyphAdvances.add(0); glyphPositions.add(width); } } /** Returns the number of glyphs from the substring that can be rendered in the specified width. * @param start The first character of the string. * @param end The last character of the string (exclusive). */ public int computeVisibleGlyphs(CharSequence str, int start, int end, float availableWidth) { BitmapFontData data = this.data; int index = start; float width = 0; Glyph lastGlyph = null; availableWidth /= data.scaleX; for (; index < end; index++) { char ch = str.charAt(index); if (ch == '[' && markupEnabled) { index++; if (!(index < end && str.charAt(index) == '[')) { // non escaped '[' while (index < end && str.charAt(index) != ']') index++; continue; } } Glyph g = data.getGlyph(ch); if (g != null) { if (lastGlyph != null) width += lastGlyph.getKerning(ch); if ((width + g.xadvance) - availableWidth > 0.001f) break; width += g.xadvance; lastGlyph = g; } } return index - start; } public void setColor(float color) { cache.setColor(color); } public void setColor(Color color) { cache.setColor(color); } public void setColor(float r, float g, float b, float a) { cache.setColor(r, g, b, a); } /** Returns the color of this font. Changing the returned color will have no affect, {@link #setColor(Color)} or * {@link #setColor(float, float, float, float)} must be used. */ public Color getColor() { return cache.getColor(); } /** Scales the font by the specified amounts on both axes <br> * <br> * Note that smoother scaling can be achieved if the texture backing the BitmapFont is using {@link TextureFilter#Linear}. The * default is Nearest, so use a BitmapFont constructor that takes a {@link TextureRegion}. * * @throws IllegalArgumentException When scaleXY is zero */ public void setScale(float scaleX, float scaleY) { if (scaleX == 0 || scaleY == 0) { throw new IllegalArgumentException("Scale must not be zero"); } BitmapFontData data = this.data; float x = scaleX / data.scaleX; float y = scaleY / data.scaleY; data.lineHeight = data.lineHeight * y; data.spaceWidth = data.spaceWidth * x; data.xHeight = data.xHeight * y; data.capHeight = data.capHeight * y; data.ascent = data.ascent * y; data.descent = data.descent * y; data.down = data.down * y; data.scaleX = scaleX; data.scaleY = scaleY; } /** Scales the font by the specified amount in both directions. * @see #setScale(float, float) * @throws IllegalArgumentException When scaleXY is zero */ public void setScale(float scaleXY) { setScale(scaleXY, scaleXY); } /** Sets the font's scale relative to the current scale. * @see #setScale(float, float) * @throws IllegalArgumentException When resulting scale is zero */ public void scale(float amount) { setScale(data.scaleX + amount, data.scaleY + amount); } public float getScaleX() { return data.scaleX; } public float getScaleY() { return data.scaleY; } /** Returns the first texture region. This is included for backwards-compatibility, and for convenience since most fonts only * use one texture page. For multi-page fonts, use getRegions(). * @return the first texture region */ // TODO: deprecate? public TextureRegion getRegion() { return regions[0]; } /** Returns the array of TextureRegions that represents each texture page of glyphs. * @return the array of texture regions; modifying it may produce undesirable results */ public TextureRegion[] getRegions() { return regions; } /** Returns the texture page at the given index. * @return the texture page at the given index */ public TextureRegion getRegion(int index) { return regions[index]; } /** Returns the line height, which is the distance from one line of text to the next. */ public float getLineHeight() { return data.lineHeight; } /** Returns the width of the space character. */ public float getSpaceWidth() { return data.spaceWidth; } /** Returns the x-height, which is the distance from the top of most lowercase characters to the baseline. */ public float getXHeight() { return data.xHeight; } /** Returns the cap height, which is the distance from the top of most uppercase characters to the baseline. Since the drawing * position is the cap height of the first line, the cap height can be used to get the location of the baseline. */ public float getCapHeight() { return data.capHeight; } /** Returns the ascent, which is the distance from the cap height to the top of the tallest glyph. */ public float getAscent() { return data.ascent; } /** Returns the descent, which is the distance from the bottom of the glyph that extends the lowest to the baseline. This number * is negative. */ public float getDescent() { return data.descent; } /** Returns true if this BitmapFont has been flipped for use with a y-down coordinate system. */ public boolean isFlipped() { return flipped; } /** Returns true if color markup is enabled for this BitmapFont */ public boolean isMarkupEnabled() { return markupEnabled; } /** Sets color markup on/off for this BitmapFont */ public void setMarkupEnabled(boolean markupEnabled) { this.markupEnabled = markupEnabled; } /** Disposes the texture used by this BitmapFont's region IF this BitmapFont created the texture. */ public void dispose() { if (ownsTexture) { for (int i = 0; i < regions.length; i++) regions[i].getTexture().dispose(); } } /** Makes the specified glyphs fixed width. This can be useful to make the numbers in a font fixed width. Eg, when horizontally * centering a score or loading percentage text, it will not jump around as different numbers are shown. */ public void setFixedWidthGlyphs(CharSequence glyphs) { BitmapFontData data = this.data; int maxAdvance = 0; for (int index = 0, end = glyphs.length(); index < end; index++) { Glyph g = data.getGlyph(glyphs.charAt(index)); if (g != null && g.xadvance > maxAdvance) maxAdvance = g.xadvance; } for (int index = 0, end = glyphs.length(); index < end; index++) { Glyph g = data.getGlyph(glyphs.charAt(index)); if (g == null) continue; g.xoffset += (maxAdvance - g.xadvance) / 2; g.xadvance = maxAdvance; g.kerning = null; } } /** Checks whether this BitmapFont data contains a given character. */ public boolean containsCharacter(char character) { return data.getGlyph(character) != null; } /** Specifies whether to use integer positions or not. Default is to use them so filtering doesn't kick in as badly. */ public void setUseIntegerPositions(boolean integer) { this.integer = integer; cache.setUseIntegerPositions(integer); } /** Checks whether this font uses integer positions for drawing. */ public boolean usesIntegerPositions() { return integer; } /** For expert usage -- returns the BitmapFontCache used by this font, for rendering to a sprite batch. This can be used, for * example, to manipulate glyph colors within a specific index. * @return the bitmap font cache used by this font */ public BitmapFontCache getCache() { return cache; } /** Gets the underlying {@link BitmapFontData} for this BitmapFont. */ public BitmapFontData getData() { return data; } /** @return whether the texture is owned by the font, font disposes the texture itself if true */ public boolean ownsTexture() { return ownsTexture; } /** Sets whether the font owns the texture or not. In case it does, the font will also dispose of the texture when * {@link #dispose()} is called. Use with care! * @param ownsTexture whether the font owns the texture */ public void setOwnsTexture(boolean ownsTexture) { this.ownsTexture = ownsTexture; } public String toString() { if (data.fontFile != null) { return data.fontFile.nameWithoutExtension(); } return super.toString(); } /** Represents a single character in a font page. */ public static class Glyph { public int id; public int srcX; public int srcY; public int width, height; public float u, v, u2, v2; public int xoffset, yoffset; public int xadvance; public byte[][] kerning; /** The index to the texture page that holds this glyph. */ public int page = 0; public int getKerning(char ch) { if (kerning != null) { byte[] page = kerning[ch >>> LOG2_PAGE_SIZE]; if (page != null) return page[ch & PAGE_SIZE - 1]; } return 0; } public void setKerning(int ch, int value) { if (kerning == null) kerning = new byte[PAGES][]; byte[] page = kerning[ch >>> LOG2_PAGE_SIZE]; if (page == null) kerning[ch >>> LOG2_PAGE_SIZE] = page = new byte[PAGE_SIZE]; page[ch & PAGE_SIZE - 1] = (byte) value; } } static int indexOf(CharSequence text, char ch, int start) { final int n = text.length(); for (; start < n; start++) if (text.charAt(start) == ch) return start; return n; } static boolean isWhitespace(char c) { switch (c) { case '\n': case '\r': case '\t': case ' ': return true; default: return false; } } /** Arbitrarily definable text boundary */ static public class TextBounds { public float width; public float height; public TextBounds() { } public TextBounds(TextBounds bounds) { set(bounds); } public void set(TextBounds bounds) { width = bounds.width; height = bounds.height; } } /** Defines possible horizontal alignments. */ static public enum HAlignment { LEFT, CENTER, RIGHT } /** Backing data for a {@link BitmapFont}. */ public static class BitmapFontData { /** The first discovered image path; included for backwards-compatibility This is the same as imagePaths[0]. * @deprecated use imagePaths[0] instead */ @Deprecated public String imagePath; /** An array of the image paths, i.e. for multiple texture pages */ public String[] imagePaths; public FileHandle fontFile; public boolean flipped; public float lineHeight; public float capHeight = 1; public float ascent; public float descent; public float down; public float scaleX = 1, scaleY = 1; public final Glyph[][] glyphs = new Glyph[PAGES][]; public float spaceWidth; public float xHeight = 1; /** Use this if you want to create BitmapFontData yourself, e.g. from stb-truetype of FreeType. */ public BitmapFontData() { } @SuppressWarnings("deprecation") public BitmapFontData(FileHandle fontFile, boolean flip) { this.fontFile = fontFile; this.flipped = flip; BufferedReader reader = new BufferedReader(new InputStreamReader(fontFile.read()), 512); try { reader.readLine(); // info String line = reader.readLine(); if (line == null) throw new GdxRuntimeException("Invalid font file: " + fontFile); String[] common = line.split(" ", 7); // we want the 6th element to be in tact; i.e. "page=N" // we only really NEED lineHeight and base if (common.length < 3) throw new GdxRuntimeException("Invalid font file: " + fontFile); if (!common[1].startsWith("lineHeight=")) throw new GdxRuntimeException("Invalid font file: " + fontFile); lineHeight = Integer.parseInt(common[1].substring(11)); if (!common[2].startsWith("base=")) throw new GdxRuntimeException("Invalid font file: " + fontFile); float baseLine = Integer.parseInt(common[2].substring(5)); // parse the pages count int imgPageCount = 1; if (common.length >= 6 && common[5] != null && common[5].startsWith("pages=")) { try { imgPageCount = Math.max(1, Integer.parseInt(common[5].substring(6))); } catch (NumberFormatException e) { // just ignore and only use one page... // somebody must have tampered with the page count >:( } } imagePaths = new String[imgPageCount]; // read each page definition for (int p = 0; p < imgPageCount; p++) { // read each "page" info line line = reader.readLine(); if (line == null) throw new GdxRuntimeException("Expected more 'page' definitions in font file " + fontFile); String[] pageLine = line.split(" ", 4); if (!pageLine[2].startsWith("file=")) throw new GdxRuntimeException("Invalid font file: " + fontFile); // we will expect ID to mean "index" -- if for some reason this is not the case, it will fuck everything up // so we need to warn the user that their BMFont output is bogus if (pageLine[1].startsWith("id=")) { try { int pageID = Integer.parseInt(pageLine[1].substring(3)); if (pageID != p) throw new GdxRuntimeException("Invalid font file: " + fontFile + " -- page ids must be indices starting at 0"); } catch (NumberFormatException e) { throw new GdxRuntimeException( "NumberFormatException on 'page id' element of " + fontFile); } } String imgFilename = null; if (pageLine[2].endsWith("\"")) { imgFilename = pageLine[2].substring(6, pageLine[2].length() - 1); } else { imgFilename = pageLine[2].substring(5, pageLine[2].length()); } String path = fontFile.parent().child(imgFilename).path().replaceAll("\\\\", "/"); if (this.imagePath == null) this.imagePath = path; imagePaths[p] = path; } descent = 0; while (true) { line = reader.readLine(); if (line == null) break; // EOF if (line.startsWith("kernings ")) break; // Starting kernings block if (!line.startsWith("char ")) continue; Glyph glyph = new Glyph(); StringTokenizer tokens = new StringTokenizer(line, " ="); tokens.nextToken(); tokens.nextToken(); int ch = Integer.parseInt(tokens.nextToken()); if (ch <= Character.MAX_VALUE) setGlyph(ch, glyph); else continue; glyph.id = ch; tokens.nextToken(); glyph.srcX = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); glyph.srcY = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); glyph.width = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); glyph.height = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); glyph.xoffset = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); if (flip) glyph.yoffset = Integer.parseInt(tokens.nextToken()); else glyph.yoffset = -(glyph.height + Integer.parseInt(tokens.nextToken())); tokens.nextToken(); glyph.xadvance = Integer.parseInt(tokens.nextToken()); // also check for page.. a little safer here since we don't want to break any old functionality // and since maybe some shitty BMFont tools won't bother writing page id?? if (tokens.hasMoreTokens()) tokens.nextToken(); if (tokens.hasMoreTokens()) { try { glyph.page = Integer.parseInt(tokens.nextToken()); } catch (NumberFormatException e) { } } if (glyph.width > 0 && glyph.height > 0) descent = Math.min(baseLine + glyph.yoffset, descent); } while (true) { line = reader.readLine(); if (line == null) break; if (!line.startsWith("kerning ")) break; StringTokenizer tokens = new StringTokenizer(line, " ="); tokens.nextToken(); tokens.nextToken(); int first = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); int second = Integer.parseInt(tokens.nextToken()); if (first < 0 || first > Character.MAX_VALUE || second < 0 || second > Character.MAX_VALUE) continue; Glyph glyph = getGlyph((char) first); tokens.nextToken(); int amount = Integer.parseInt(tokens.nextToken()); if (glyph != null) { // it appears BMFont outputs kerning for glyph pairs not contained in the font, hence the null // check glyph.setKerning(second, amount); } } Glyph spaceGlyph = getGlyph(' '); if (spaceGlyph == null) { spaceGlyph = new Glyph(); spaceGlyph.id = (int) ' '; Glyph xadvanceGlyph = getGlyph('l'); if (xadvanceGlyph == null) xadvanceGlyph = getFirstGlyph(); spaceGlyph.xadvance = xadvanceGlyph.xadvance; setGlyph(' ', spaceGlyph); } spaceWidth = spaceGlyph != null ? spaceGlyph.xadvance + spaceGlyph.width : 1; Glyph xGlyph = null; for (int i = 0; i < xChars.length; i++) { xGlyph = getGlyph(xChars[i]); if (xGlyph != null) break; } if (xGlyph == null) xGlyph = getFirstGlyph(); xHeight = xGlyph.height; Glyph capGlyph = null; for (int i = 0; i < capChars.length; i++) { capGlyph = getGlyph(capChars[i]); if (capGlyph != null) break; } if (capGlyph == null) { for (Glyph[] page : this.glyphs) { if (page == null) continue; for (Glyph glyph : page) { if (glyph == null || glyph.height == 0 || glyph.width == 0) continue; capHeight = Math.max(capHeight, glyph.height); } } } else capHeight = capGlyph.height; ascent = baseLine - capHeight; down = -lineHeight; if (flip) { ascent = -ascent; down = -down; } } catch (Exception ex) { throw new GdxRuntimeException("Error loading font file: " + fontFile, ex); } finally { StreamUtils.closeQuietly(reader); } } public void setGlyph(int ch, Glyph glyph) { Glyph[] page = glyphs[ch / PAGE_SIZE]; if (page == null) glyphs[ch / PAGE_SIZE] = page = new Glyph[PAGE_SIZE]; page[ch & PAGE_SIZE - 1] = glyph; } public Glyph getFirstGlyph() { for (Glyph[] page : this.glyphs) { if (page == null) continue; for (Glyph glyph : page) { if (glyph == null || glyph.height == 0 || glyph.width == 0) continue; return glyph; } } throw new GdxRuntimeException("No glyphs found!"); } /** Returns the glyph for the specified character, or null if no such glyph exists. */ public Glyph getGlyph(char ch) { Glyph[] page = glyphs[ch / PAGE_SIZE]; if (page != null) return page[ch & PAGE_SIZE - 1]; return null; } /** Returns the first image path; included for backwards-compatibility. Use getImagePath(int) instead. * @return the first image path in the array * @deprecated use getImagePath(int index) instead */ @Deprecated public String getImagePath() { return imagePath; } /** Returns the image path for the texture page at the given index. * @param index the index of the page, AKA the "id" in the BMFont file * @return the texture page */ public String getImagePath(int index) { return imagePaths[index]; } public String[] getImagePaths() { return imagePaths; } public FileHandle getFontFile() { return fontFile; } } }