/*
* 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.IOException;
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;
/**
* 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 SpriteBatch}. 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 region;
private final BitmapFontCache cache = new BitmapFontCache(this);
private boolean flipped;
private boolean integer;
private boolean ownsTexture;
/**
* 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!
*
* @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.
*
* @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), 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.s
*/
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 is read from the BitmapFontData. The dispose() method will not dispose the texture of the
* region if the region is != null.
*
* @param data
* @param region
* @param integer
*/
public BitmapFont(BitmapFontData data, TextureRegion region, boolean integer) {
this.region = region == null ? new TextureRegion(new Texture(Gdx.files.internal(data.imagePath), false))
: region;
this.flipped = data.flipped;
this.data = data;
this.integer = integer;
cache.setUseIntegerPositions(integer);
load(data);
ownsTexture = region == null;
}
private void load(BitmapFontData data) {
float invTexWidth = 1.0f / region.getTexture().getWidth();
float invTexHeight = 1.0f / region.getTexture().getHeight();
float u = region.u;
float v = region.v;
float offsetX = 0, offsetY = 0;
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;
}
for (Glyph[] page : data.glyphs) {
if (page == null)
continue;
for (Glyph glyph : page) {
if (glyph == null)
continue;
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(SpriteBatch spriteBatch, CharSequence str, float x, float y) {
cache.clear();
TextBounds bounds = cache.addText(str, x, y, 0, str.length());
cache.draw(spriteBatch);
return bounds;
}
/**
* Draws a string at the specified position.
*
* @see BitmapFontCache#addText(CharSequence, float, float, int, int)
*/
public TextBounds draw(SpriteBatch spriteBatch, CharSequence str, float x, float y, int start, int end) {
cache.clear();
TextBounds bounds = cache.addText(str, x, y, start, end);
cache.draw(spriteBatch);
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(SpriteBatch spriteBatch, CharSequence str, float x, float y) {
cache.clear();
TextBounds bounds = cache.addMultiLineText(str, x, y, 0, HAlignment.LEFT);
cache.draw(spriteBatch);
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(SpriteBatch spriteBatch, CharSequence str, float x, float y, float alignmentWidth,
HAlignment alignment) {
cache.clear();
TextBounds bounds = cache.addMultiLineText(str, x, y, alignmentWidth, alignment);
cache.draw(spriteBatch);
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(SpriteBatch spriteBatch, CharSequence str, float x, float y, float wrapWidth) {
cache.clear();
TextBounds bounds = cache.addWrappedText(str, x, y, wrapWidth, HAlignment.LEFT);
cache.draw(spriteBatch);
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(SpriteBatch spriteBatch, CharSequence str, float x, float y, float wrapWidth,
HAlignment alignment) {
cache.clear();
TextBounds bounds = cache.addWrappedText(str, x, y, wrapWidth, alignment);
cache.draw(spriteBatch);
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());
}
/**
* 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) {
lastGlyph = data.getGlyph(str.charAt(start++));
if (lastGlyph != null) {
width = lastGlyph.xadvance;
break;
}
}
while (start < end) {
char ch = str.charAt(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.
*/
public TextBounds getWrappedBounds(CharSequence str, float wrapWidth, TextBounds textBounds) {
if (wrapWidth <= 0)
wrapWidth = Integer.MAX_VALUE;
float down = this.data.down;
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;
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);
if ((width + g.xadvance) - availableWidth > 0.001f)
break;
width += g.xadvance;
lastGlyph = g;
}
}
} 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;
float xadvance = g.xadvance * scaleX;
if ((width + xadvance) - availableWidth > 0.001f)
break;
width += 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();
}
public void setScale(float scaleX, float scaleY) {
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.<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}.
*/
public void setScale(float scaleXY) {
setScale(scaleXY, scaleXY);
}
/** Sets the font's scale relative to the current scale. */
public void scale(float amount) {
setScale(data.scaleX + amount, data.scaleY + amount);
}
public float getScaleX() {
return data.scaleX;
}
public float getScaleY() {
return data.scaleY;
}
public TextureRegion getRegion() {
return region;
}
/** 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;
}
/** Disposes the texture used by this BitmapFont's region IF this BitmapFont created the texture. */
public void dispose() {
if (ownsTexture)
region.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;
}
}
/** @return true if the character is contained in this font. */
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);
}
/** @return whether this font uses integer positions for drawing. */
public boolean usesIntegerPositions() {
return integer;
}
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 static class Glyph {
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;
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;
}
}
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;
}
}
static public enum HAlignment {
LEFT, CENTER, RIGHT
}
public static class BitmapFontData {
public String imagePath;
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() {
}
public BitmapFontData(FileHandle fontFile, boolean flip) {
this.fontFile = fontFile;
this.flipped = flip;
BufferedReader reader = new BufferedReader(new InputStreamReader(fontFile.read()), 512);
try {
String infor_line = reader.readLine(); // info
String[] xinfor = infor_line.split(" ");
int padding_left = 0, padding_right = 0, padding_top = 0, padding_bottom = 0, padding_temp = 0;
for (int i = 0; i < xinfor.length; i++) {
if (xinfor[i].startsWith("padding=")) {
String[] paddings = xinfor[i].substring(8).split(",");
padding_left = Integer.parseInt(paddings[1]);
padding_right = Integer.parseInt(paddings[3]);
padding_top = Integer.parseInt(paddings[0]);
padding_bottom = Integer.parseInt(paddings[2]);
if (paddings.length > 4) {
padding_temp = Integer.parseInt(paddings[4]);
}
break;
}
}
String line = reader.readLine();
if (line == null)
throw new GdxRuntimeException("Invalid font file: " + fontFile);
String[] common = line.split(" ", 4);
if (common.length < 4)
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)) - padding_top - padding_bottom - padding_temp;
if (!common[2].startsWith("base="))
throw new GdxRuntimeException("Invalid font file: " + fontFile);
int baseLine = Integer.parseInt(common[2].substring(5));
line = reader.readLine();
if (line == null)
throw new GdxRuntimeException("Invalid font file: " + fontFile);
String[] pageLine = line.split(" ", 4);
if (!pageLine[2].startsWith("file="))
throw new GdxRuntimeException("Invalid font file: " + 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());
}
imagePath = fontFile.parent().child(imgFilename).path().replaceAll("\\\\", "/");
descent = 0;
while (true) {
line = reader.readLine();
if (line == null)
break;
if (line.startsWith("kernings "))
break;
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;
if (ch != 32) {
tokens.nextToken();
glyph.srcX = Integer.parseInt(tokens.nextToken()) + padding_left;
tokens.nextToken();
glyph.srcY = Integer.parseInt(tokens.nextToken()) + padding_top;
tokens.nextToken();
glyph.width = Integer.parseInt(tokens.nextToken()) - padding_left - padding_right;
tokens.nextToken();
glyph.height = Integer.parseInt(tokens.nextToken()) - padding_bottom - padding_top;
tokens.nextToken();
glyph.xoffset = Integer.parseInt(tokens.nextToken()) + padding_left;
tokens.nextToken();
if (flip)
glyph.yoffset = (Integer.parseInt(tokens.nextToken()) + padding_top + padding_temp);
else
glyph.yoffset = -(glyph.height + (Integer.parseInt(tokens.nextToken()) + padding_top + padding_temp));
tokens.nextToken();
glyph.xadvance = Integer.parseInt(tokens.nextToken()) - padding_left - padding_right - 1;
if (glyph.width > 0 && glyph.height > 0)
descent = Math.min(baseLine + glyph.yoffset, descent);
} else {
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()) - padding_left - padding_right - 1;
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());
glyph.setKerning(second, amount);
}
Glyph spaceGlyph = getGlyph(' ');
if (spaceGlyph == null) {
spaceGlyph = new Glyph();
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) {
ex.printStackTrace();
throw new GdxRuntimeException("Error loading font file: " + fontFile, ex);
} finally {
try {
reader.close();
} catch (IOException ignored) {
}
}
}
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!");
}
public Glyph getGlyph(char ch) {
Glyph[] page = glyphs[ch / PAGE_SIZE];
if (page != null)
return page[ch & PAGE_SIZE - 1];
return null;
}
public String getImagePath() {
return imagePath;
}
public FileHandle getFontFile() {
return fontFile;
}
}
}