• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2008-2010, Matthias Mann
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
7  * conditions are met:
8  *
9  * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
10  * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
11  * disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Matthias Mann nor
12  * the names of its contributors may be used to endorse or promote products derived from this software without specific prior
13  * written permission.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
16  * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
17  * SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
19  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
20  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
21  */
22 
23 package com.badlogic.gdx.graphics.g2d;
24 
25 import java.io.BufferedReader;
26 import java.io.InputStreamReader;
27 import java.util.StringTokenizer;
28 
29 import com.badlogic.gdx.Gdx;
30 import com.badlogic.gdx.files.FileHandle;
31 import com.badlogic.gdx.graphics.Color;
32 import com.badlogic.gdx.graphics.Texture;
33 import com.badlogic.gdx.graphics.Texture.TextureFilter;
34 import com.badlogic.gdx.graphics.g2d.GlyphLayout.GlyphRun;
35 import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
36 import com.badlogic.gdx.utils.Array;
37 import com.badlogic.gdx.utils.Disposable;
38 import com.badlogic.gdx.utils.FloatArray;
39 import com.badlogic.gdx.utils.GdxRuntimeException;
40 import com.badlogic.gdx.utils.StreamUtils;
41 
42 /** Renders bitmap fonts. The font consists of 2 files: an image file or {@link TextureRegion} containing the glyphs and a file in
43  * the AngleCode BMFont text format that describes where each glyph is on the image.
44  * <p>
45  * Text is drawn using a {@link Batch}. Text can be cached in a {@link BitmapFontCache} for faster rendering of static text, which
46  * saves needing to compute the location of each glyph each frame.
47  * <p>
48  * * The texture for a BitmapFont loaded from a file is managed. {@link #dispose()} must be called to free the texture when no
49  * longer needed. A BitmapFont loaded using a {@link TextureRegion} is managed if the region's texture is managed. Disposing the
50  * BitmapFont disposes the region's texture, which may not be desirable if the texture is still being used elsewhere.
51  * <p>
52  * The code was originally based on Matthias Mann's TWL BitmapFont class. Thanks for sharing, Matthias! :)
53  * @author Nathan Sweet
54  * @author Matthias Mann */
55 public class BitmapFont implements Disposable {
56 	static private final int LOG2_PAGE_SIZE = 9;
57 	static private final int PAGE_SIZE = 1 << LOG2_PAGE_SIZE;
58 	static private final int PAGES = 0x10000 / PAGE_SIZE;
59 
60 	final BitmapFontData data;
61 	Array<TextureRegion> regions;
62 	private final BitmapFontCache cache;
63 	private boolean flipped;
64 	boolean integer;
65 	private boolean ownsTexture;
66 
67 	/** Creates a BitmapFont using the default 15pt Arial font included in the libgdx JAR file. This is convenient to easily
68 	 * display text without bothering without generating a bitmap font yourself. */
BitmapFont()69 	public BitmapFont () {
70 		this(Gdx.files.classpath("com/badlogic/gdx/utils/arial-15.fnt"), Gdx.files.classpath("com/badlogic/gdx/utils/arial-15.png"),
71 			false, true);
72 	}
73 
74 	/** Creates a BitmapFont using the default 15pt Arial font included in the libgdx JAR file. This is convenient to easily
75 	 * display text without bothering without generating a bitmap font yourself.
76 	 * @param flip If true, the glyphs will be flipped for use with a perspective where 0,0 is the upper left corner. */
BitmapFont(boolean flip)77 	public BitmapFont (boolean flip) {
78 		this(Gdx.files.classpath("com/badlogic/gdx/utils/arial-15.fnt"), Gdx.files.classpath("com/badlogic/gdx/utils/arial-15.png"),
79 			flip, true);
80 	}
81 
82 	/** Creates a BitmapFont with the glyphs relative to the specified region. If the region is null, the glyph textures are loaded
83 	 * from the image file given in the font file. The {@link #dispose()} method will not dispose the region's texture in this
84 	 * case!
85 	 * <p>
86 	 * The font data is not flipped.
87 	 * @param fontFile the font definition file
88 	 * @param region The texture region containing the glyphs. The glyphs must be relative to the lower left corner (ie, the region
89 	 *           should not be flipped). If the region is null the glyph images are loaded from the image path in the font file. */
BitmapFont(FileHandle fontFile, TextureRegion region)90 	public BitmapFont (FileHandle fontFile, TextureRegion region) {
91 		this(fontFile, region, false);
92 	}
93 
94 	/** Creates a BitmapFont with the glyphs relative to the specified region. If the region is null, the glyph textures are loaded
95 	 * from the image file given in the font file. The {@link #dispose()} method will not dispose the region's texture in this
96 	 * case!
97 	 * @param region The texture region containing the glyphs. The glyphs must be relative to the lower left corner (ie, the region
98 	 *           should not be flipped). If the region is null the glyph images are loaded from the image path in the font file.
99 	 * @param flip If true, the glyphs will be flipped for use with a perspective where 0,0 is the upper left corner. */
BitmapFont(FileHandle fontFile, TextureRegion region, boolean flip)100 	public BitmapFont (FileHandle fontFile, TextureRegion region, boolean flip) {
101 		this(new BitmapFontData(fontFile, flip), region, true);
102 	}
103 
104 	/** Creates a BitmapFont from a BMFont file. The image file name is read from the BMFont file and the image is loaded from the
105 	 * same directory. The font data is not flipped. */
BitmapFont(FileHandle fontFile)106 	public BitmapFont (FileHandle fontFile) {
107 		this(fontFile, false);
108 	}
109 
110 	/** Creates a BitmapFont from a BMFont file. The image file name is read from the BMFont file and the image is loaded from the
111 	 * same directory.
112 	 * @param flip If true, the glyphs will be flipped for use with a perspective where 0,0 is the upper left corner. */
BitmapFont(FileHandle fontFile, boolean flip)113 	public BitmapFont (FileHandle fontFile, boolean flip) {
114 		this(new BitmapFontData(fontFile, flip), (TextureRegion)null, true);
115 	}
116 
117 	/** Creates a BitmapFont from a BMFont file, using the specified image for glyphs. Any image specified in the BMFont file is
118 	 * ignored.
119 	 * @param flip If true, the glyphs will be flipped for use with a perspective where 0,0 is the upper left corner. */
BitmapFont(FileHandle fontFile, FileHandle imageFile, boolean flip)120 	public BitmapFont (FileHandle fontFile, FileHandle imageFile, boolean flip) {
121 		this(fontFile, imageFile, flip, true);
122 	}
123 
124 	/** Creates a BitmapFont from a BMFont file, using the specified image for glyphs. Any image specified in the BMFont file is
125 	 * ignored.
126 	 * @param flip If true, the glyphs will be flipped for use with a perspective where 0,0 is the upper left corner.
127 	 * @param integer If true, rendering positions will be at integer values to avoid filtering artifacts. */
BitmapFont(FileHandle fontFile, FileHandle imageFile, boolean flip, boolean integer)128 	public BitmapFont (FileHandle fontFile, FileHandle imageFile, boolean flip, boolean integer) {
129 		this(new BitmapFontData(fontFile, flip), new TextureRegion(new Texture(imageFile, false)), integer);
130 		ownsTexture = true;
131 	}
132 
133 	/** Constructs a new BitmapFont from the given {@link BitmapFontData} and {@link TextureRegion}. If the TextureRegion is null,
134 	 * the image path(s) will be read from the BitmapFontData. The dispose() method will not dispose the texture of the region(s)
135 	 * if the region is != null.
136 	 * <p>
137 	 * Passing a single TextureRegion assumes that your font only needs a single texture page. If you need to support multiple
138 	 * pages, either let the Font read the images themselves (by specifying null as the TextureRegion), or by specifying each page
139 	 * manually with the TextureRegion[] constructor.
140 	 * @param integer If true, rendering positions will be at integer values to avoid filtering artifacts. */
BitmapFont(BitmapFontData data, TextureRegion region, boolean integer)141 	public BitmapFont (BitmapFontData data, TextureRegion region, boolean integer) {
142 		this(data, region != null ? Array.with(region) : null, integer);
143 	}
144 
145 	/** Constructs a new BitmapFont from the given {@link BitmapFontData} and array of {@link TextureRegion}. If the TextureRegion
146 	 * is null or empty, the image path(s) will be read from the BitmapFontData. The dispose() method will not dispose the texture
147 	 * of the region(s) if the regions array is != null and not empty.
148 	 * @param integer If true, rendering positions will be at integer values to avoid filtering artifacts. */
BitmapFont(BitmapFontData data, Array<TextureRegion> pageRegions, boolean integer)149 	public BitmapFont (BitmapFontData data, Array<TextureRegion> pageRegions, boolean integer) {
150 		this.flipped = data.flipped;
151 		this.data = data;
152 		this.integer = integer;
153 
154 		if (pageRegions == null || pageRegions.size == 0) {
155 			// Load each path.
156 			int n = data.imagePaths.length;
157 			regions = new Array(n);
158 			for (int i = 0; i < n; i++) {
159 				FileHandle file;
160 				if (data.fontFile == null)
161 					file = Gdx.files.internal(data.imagePaths[i]);
162 				else
163 					file = Gdx.files.getFileHandle(data.imagePaths[i], data.fontFile.type());
164 				regions.add(new TextureRegion(new Texture(file, false)));
165 			}
166 			ownsTexture = true;
167 		} else {
168 			regions = pageRegions;
169 			ownsTexture = false;
170 		}
171 
172 		cache = newFontCache();
173 
174 		load(data);
175 	}
176 
load(BitmapFontData data)177 	protected void load (BitmapFontData data) {
178 		for (Glyph[] page : data.glyphs) {
179 			if (page == null) continue;
180 			for (Glyph glyph : page)
181 				if (glyph != null) data.setGlyphRegion(glyph, regions.get(glyph.page));
182 		}
183 		if (data.missingGlyph != null) data.setGlyphRegion(data.missingGlyph, regions.get(data.missingGlyph.page));
184 	}
185 
186 	/** Draws text at the specified position.
187 	 * @see BitmapFontCache#addText(CharSequence, float, float) */
draw(Batch batch, CharSequence str, float x, float y)188 	public GlyphLayout draw (Batch batch, CharSequence str, float x, float y) {
189 		cache.clear();
190 		GlyphLayout layout = cache.addText(str, x, y);
191 		cache.draw(batch);
192 		return layout;
193 	}
194 
195 	/** Draws text at the specified position.
196 	 * @see BitmapFontCache#addText(CharSequence, float, float, int, int, float, int, boolean, String) */
draw(Batch batch, CharSequence str, float x, float y, float targetWidth, int halign, boolean wrap)197 	public GlyphLayout draw (Batch batch, CharSequence str, float x, float y, float targetWidth, int halign, boolean wrap) {
198 		cache.clear();
199 		GlyphLayout layout = cache.addText(str, x, y, targetWidth, halign, wrap);
200 		cache.draw(batch);
201 		return layout;
202 	}
203 
204 	/** Draws text at the specified position.
205 	 * @see BitmapFontCache#addText(CharSequence, float, float, int, int, float, int, boolean, String) */
draw(Batch batch, CharSequence str, float x, float y, int start, int end, float targetWidth, int halign, boolean wrap)206 	public GlyphLayout draw (Batch batch, CharSequence str, float x, float y, int start, int end, float targetWidth, int halign,
207 		boolean wrap) {
208 		cache.clear();
209 		GlyphLayout layout = cache.addText(str, x, y, start, end, targetWidth, halign, wrap);
210 		cache.draw(batch);
211 		return layout;
212 	}
213 
214 	/** Draws text at the specified position.
215 	 * @see BitmapFontCache#addText(CharSequence, float, float, int, int, float, int, boolean, String) */
draw(Batch batch, CharSequence str, float x, float y, int start, int end, float targetWidth, int halign, boolean wrap, String truncate)216 	public GlyphLayout draw (Batch batch, CharSequence str, float x, float y, int start, int end, float targetWidth, int halign,
217 		boolean wrap, String truncate) {
218 		cache.clear();
219 		GlyphLayout layout = cache.addText(str, x, y, start, end, targetWidth, halign, wrap, truncate);
220 		cache.draw(batch);
221 		return layout;
222 	}
223 
224 	/** Draws text at the specified position.
225 	 * @see BitmapFontCache#addText(CharSequence, float, float, int, int, float, int, boolean, String) */
draw(Batch batch, GlyphLayout layout, float x, float y)226 	public void draw (Batch batch, GlyphLayout layout, float x, float y) {
227 		cache.clear();
228 		cache.addText(layout, x, y);
229 		cache.draw(batch);
230 	}
231 
232 	/** Returns the color of text drawn with this font. */
getColor()233 	public Color getColor () {
234 		return cache.getColor();
235 	}
236 
237 	/** A convenience method for setting the font color. The color can also be set by modifying {@link #getColor()}. */
setColor(Color color)238 	public void setColor (Color color) {
239 		cache.getColor().set(color);
240 	}
241 
242 	/** A convenience method for setting the font color. The color can also be set by modifying {@link #getColor()}. */
setColor(float r, float g, float b, float a)243 	public void setColor (float r, float g, float b, float a) {
244 		cache.getColor().set(r, g, b, a);
245 	}
246 
getScaleX()247 	public float getScaleX () {
248 		return data.scaleX;
249 	}
250 
getScaleY()251 	public float getScaleY () {
252 		return data.scaleY;
253 	}
254 
255 	/** Returns the first texture region. This is included for backwards compatibility, and for convenience since most fonts only
256 	 * use one texture page. For multi-page fonts, use {@link #getRegions()}.
257 	 * @return the first texture region */
getRegion()258 	public TextureRegion getRegion () {
259 		return regions.first();
260 	}
261 
262 	/** Returns the array of TextureRegions that represents each texture page of glyphs.
263 	 * @return the array of texture regions; modifying it may produce undesirable results */
getRegions()264 	public Array<TextureRegion> getRegions () {
265 		return regions;
266 	}
267 
268 	/** Returns the texture page at the given index.
269 	 * @return the texture page at the given index */
getRegion(int index)270 	public TextureRegion getRegion (int index) {
271 		return regions.get(index);
272 	}
273 
274 	/** Returns the line height, which is the distance from one line of text to the next. */
getLineHeight()275 	public float getLineHeight () {
276 		return data.lineHeight;
277 	}
278 
279 	/** Returns the width of the space character. */
getSpaceWidth()280 	public float getSpaceWidth () {
281 		return data.spaceWidth;
282 	}
283 
284 	/** Returns the x-height, which is the distance from the top of most lowercase characters to the baseline. */
getXHeight()285 	public float getXHeight () {
286 		return data.xHeight;
287 	}
288 
289 	/** Returns the cap height, which is the distance from the top of most uppercase characters to the baseline. Since the drawing
290 	 * position is the cap height of the first line, the cap height can be used to get the location of the baseline. */
getCapHeight()291 	public float getCapHeight () {
292 		return data.capHeight;
293 	}
294 
295 	/** Returns the ascent, which is the distance from the cap height to the top of the tallest glyph. */
getAscent()296 	public float getAscent () {
297 		return data.ascent;
298 	}
299 
300 	/** Returns the descent, which is the distance from the bottom of the glyph that extends the lowest to the baseline. This
301 	 * number is negative. */
getDescent()302 	public float getDescent () {
303 		return data.descent;
304 	}
305 
306 	/** Returns true if this BitmapFont has been flipped for use with a y-down coordinate system. */
isFlipped()307 	public boolean isFlipped () {
308 		return flipped;
309 	}
310 
311 	/** Disposes the texture used by this BitmapFont's region IF this BitmapFont created the texture. */
dispose()312 	public void dispose () {
313 		if (ownsTexture) {
314 			for (int i = 0; i < regions.size; i++)
315 				regions.get(i).getTexture().dispose();
316 		}
317 	}
318 
319 	/** Makes the specified glyphs fixed width. This can be useful to make the numbers in a font fixed width. Eg, when horizontally
320 	 * centering a score or loading percentage text, it will not jump around as different numbers are shown. */
setFixedWidthGlyphs(CharSequence glyphs)321 	public void setFixedWidthGlyphs (CharSequence glyphs) {
322 		BitmapFontData data = this.data;
323 		int maxAdvance = 0;
324 		for (int index = 0, end = glyphs.length(); index < end; index++) {
325 			Glyph g = data.getGlyph(glyphs.charAt(index));
326 			if (g != null && g.xadvance > maxAdvance) maxAdvance = g.xadvance;
327 		}
328 		for (int index = 0, end = glyphs.length(); index < end; index++) {
329 			Glyph g = data.getGlyph(glyphs.charAt(index));
330 			if (g == null) continue;
331 			g.xoffset += Math.round((maxAdvance - g.xadvance) / 2);
332 			g.xadvance = maxAdvance;
333 			g.kerning = null;
334 			g.fixedWidth = true;
335 		}
336 	}
337 
338 	/** Specifies whether to use integer positions. Default is to use them so filtering doesn't kick in as badly. */
setUseIntegerPositions(boolean integer)339 	public void setUseIntegerPositions (boolean integer) {
340 		this.integer = integer;
341 		cache.setUseIntegerPositions(integer);
342 	}
343 
344 	/** Checks whether this font uses integer positions for drawing. */
usesIntegerPositions()345 	public boolean usesIntegerPositions () {
346 		return integer;
347 	}
348 
349 	/** For expert usage -- returns the BitmapFontCache used by this font, for rendering to a sprite batch. This can be used, for
350 	 * example, to manipulate glyph colors within a specific index.
351 	 * @return the bitmap font cache used by this font */
getCache()352 	public BitmapFontCache getCache () {
353 		return cache;
354 	}
355 
356 	/** Gets the underlying {@link BitmapFontData} for this BitmapFont. */
getData()357 	public BitmapFontData getData () {
358 		return data;
359 	}
360 
361 	/** @return whether the texture is owned by the font, font disposes the texture itself if true */
ownsTexture()362 	public boolean ownsTexture () {
363 		return ownsTexture;
364 	}
365 
366 	/** Sets whether the font owns the texture. In case it does, the font will also dispose of the texture when {@link #dispose()}
367 	 * is called. Use with care!
368 	 * @param ownsTexture whether the font owns the texture */
setOwnsTexture(boolean ownsTexture)369 	public void setOwnsTexture (boolean ownsTexture) {
370 		this.ownsTexture = ownsTexture;
371 	}
372 
373 	/** Creates a new BitmapFontCache for this font. Using this method allows the font to provide the BitmapFontCache
374 	 * implementation to customize rendering.
375 	 * <p>
376 	 * Note this method is called by the BitmapFont constructors. If a subclass overrides this method, it will be called before the
377 	 * subclass constructors. */
newFontCache()378 	public BitmapFontCache newFontCache () {
379 		return new BitmapFontCache(this, integer);
380 	}
381 
toString()382 	public String toString () {
383 		if (data.fontFile != null) return data.fontFile.nameWithoutExtension();
384 		return super.toString();
385 	}
386 
387 	/** Represents a single character in a font page. */
388 	public static class Glyph {
389 		public int id;
390 		public int srcX;
391 		public int srcY;
392 		public int width, height;
393 		public float u, v, u2, v2;
394 		public int xoffset, yoffset;
395 		public int xadvance;
396 		public byte[][] kerning;
397 		public boolean fixedWidth;
398 
399 		/** The index to the texture page that holds this glyph. */
400 		public int page = 0;
401 
getKerning(char ch)402 		public int getKerning (char ch) {
403 			if (kerning != null) {
404 				byte[] page = kerning[ch >>> LOG2_PAGE_SIZE];
405 				if (page != null) return page[ch & PAGE_SIZE - 1];
406 			}
407 			return 0;
408 		}
409 
setKerning(int ch, int value)410 		public void setKerning (int ch, int value) {
411 			if (kerning == null) kerning = new byte[PAGES][];
412 			byte[] page = kerning[ch >>> LOG2_PAGE_SIZE];
413 			if (page == null) kerning[ch >>> LOG2_PAGE_SIZE] = page = new byte[PAGE_SIZE];
414 			page[ch & PAGE_SIZE - 1] = (byte)value;
415 		}
416 
toString()417 		public String toString () {
418 			return Character.toString((char)id);
419 		}
420 	}
421 
indexOf(CharSequence text, char ch, int start)422 	static int indexOf (CharSequence text, char ch, int start) {
423 		final int n = text.length();
424 		for (; start < n; start++)
425 			if (text.charAt(start) == ch) return start;
426 		return n;
427 	}
428 
429 	/** Backing data for a {@link BitmapFont}. */
430 	static public class BitmapFontData {
431 		/** An array of the image paths, for multiple texture pages. */
432 		public String[] imagePaths;
433 		public FileHandle fontFile;
434 		public boolean flipped;
435 		public float padTop, padRight, padBottom, padLeft;
436 		/** The distance from one line of text to the next. To set this value, use {@link #setLineHeight(float)}. */
437 		public float lineHeight;
438 		/** The distance from the top of most uppercase characters to the baseline. Since the drawing position is the cap height of
439 		 * the first line, the cap height can be used to get the location of the baseline. */
440 		public float capHeight = 1;
441 		/** The distance from the cap height to the top of the tallest glyph. */
442 		public float ascent;
443 		/** The distance from the bottom of the glyph that extends the lowest to the baseline. This number is negative. */
444 		public float descent;
445 		public float down;
446 		public float scaleX = 1, scaleY = 1;
447 		public boolean markupEnabled;
448 		/** The amount to add to the glyph X position when drawing a cursor between glyphs. This field is not set by the BMFont
449 		 * file, it needs to be set manually depending on how the glyphs are rendered on the backing textures. */
450 		public float cursorX;
451 
452 		public final Glyph[][] glyphs = new Glyph[PAGES][];
453 		/** The glyph to display for characters not in the font. May be null. */
454 		public Glyph missingGlyph;
455 
456 		/** The width of the space character. */
457 		public float spaceWidth;
458 		/** The x-height, which is the distance from the top of most lowercase characters to the baseline. */
459 		public float xHeight = 1;
460 
461 		/** Additional characters besides whitespace where text is wrapped. Eg, a hypen (-). */
462 		public char[] breakChars;
463 		public char[] xChars = {'x', 'e', 'a', 'o', 'n', 's', 'r', 'c', 'u', 'm', 'v', 'w', 'z'};
464 		public char[] capChars = {'M', 'N', 'B', 'D', 'C', 'E', 'F', 'K', 'A', 'G', 'H', 'I', 'J', 'L', 'O', 'P', 'Q', 'R', 'S',
465 			'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
466 
467 		/** Creates an empty BitmapFontData for configuration before calling {@link #load(FileHandle, boolean)}, to subclass, or to
468 		 * populate yourself, e.g. using stb-truetype or FreeType. */
BitmapFontData()469 		public BitmapFontData () {
470 		}
471 
BitmapFontData(FileHandle fontFile, boolean flip)472 		public BitmapFontData (FileHandle fontFile, boolean flip) {
473 			this.fontFile = fontFile;
474 			this.flipped = flip;
475 			load(fontFile, flip);
476 		}
477 
load(FileHandle fontFile, boolean flip)478 		public void load (FileHandle fontFile, boolean flip) {
479 			if (imagePaths != null) throw new IllegalStateException("Already loaded.");
480 
481 			BufferedReader reader = new BufferedReader(new InputStreamReader(fontFile.read()), 512);
482 			try {
483 				String line = reader.readLine(); // info
484 				if (line == null) throw new GdxRuntimeException("File is empty.");
485 
486 				line = line.substring(line.indexOf("padding=") + 8);
487 				String[] padding = line.substring(0, line.indexOf(' ')).split(",", 4);
488 				if (padding.length != 4) throw new GdxRuntimeException("Invalid padding.");
489 				padTop = Integer.parseInt(padding[0]);
490 				padLeft = Integer.parseInt(padding[1]);
491 				padBottom = Integer.parseInt(padding[2]);
492 				padRight = Integer.parseInt(padding[3]);
493 				float padY = padTop + padBottom;
494 
495 				line = reader.readLine();
496 				if (line == null) throw new GdxRuntimeException("Missing common header.");
497 				String[] common = line.split(" ", 7); // At most we want the 6th element; i.e. "page=N"
498 
499 				// At least lineHeight and base are required.
500 				if (common.length < 3) throw new GdxRuntimeException("Invalid common header.");
501 
502 				if (!common[1].startsWith("lineHeight=")) throw new GdxRuntimeException("Missing: lineHeight");
503 				lineHeight = Integer.parseInt(common[1].substring(11));
504 
505 				if (!common[2].startsWith("base=")) throw new GdxRuntimeException("Missing: base");
506 				float baseLine = Integer.parseInt(common[2].substring(5));
507 
508 				int pageCount = 1;
509 				if (common.length >= 6 && common[5] != null && common[5].startsWith("pages=")) {
510 					try {
511 						pageCount = Math.max(1, Integer.parseInt(common[5].substring(6)));
512 					} catch (NumberFormatException ignored) { // Use one page.
513 					}
514 				}
515 
516 				imagePaths = new String[pageCount];
517 
518 				// Read each page definition.
519 				for (int p = 0; p < pageCount; p++) {
520 					// Read each "page" info line.
521 					line = reader.readLine();
522 					if (line == null) throw new GdxRuntimeException("Missing additional page definitions.");
523 					String[] pageLine = line.split(" ", 4);
524 					if (!pageLine[2].startsWith("file=")) throw new GdxRuntimeException("Missing: file");
525 
526 					// Expect ID to mean "index".
527 					if (pageLine[1].startsWith("id=")) {
528 						try {
529 							int pageID = Integer.parseInt(pageLine[1].substring(3));
530 							if (pageID != p)
531 								throw new GdxRuntimeException("Page IDs must be indices starting at 0: " + pageLine[1].substring(3));
532 						} catch (NumberFormatException ex) {
533 							throw new GdxRuntimeException("Invalid page id: " + pageLine[1].substring(3), ex);
534 						}
535 					}
536 
537 					String fileName = null;
538 					if (pageLine[2].endsWith("\"")) {
539 						fileName = pageLine[2].substring(6, pageLine[2].length() - 1);
540 					} else {
541 						fileName = pageLine[2].substring(5, pageLine[2].length());
542 					}
543 
544 					imagePaths[p] = fontFile.parent().child(fileName).path().replaceAll("\\\\", "/");
545 				}
546 				descent = 0;
547 
548 				while (true) {
549 					line = reader.readLine();
550 					if (line == null) break; // EOF
551 					if (line.startsWith("kernings ")) break; // Starting kernings block.
552 					if (!line.startsWith("char ")) continue;
553 
554 					Glyph glyph = new Glyph();
555 
556 					StringTokenizer tokens = new StringTokenizer(line, " =");
557 					tokens.nextToken();
558 					tokens.nextToken();
559 					int ch = Integer.parseInt(tokens.nextToken());
560 					if (ch <= 0)
561 						missingGlyph = glyph;
562 					else if (ch <= Character.MAX_VALUE)
563 						setGlyph(ch, glyph);
564 					else
565 						continue;
566 					glyph.id = ch;
567 					tokens.nextToken();
568 					glyph.srcX = Integer.parseInt(tokens.nextToken());
569 					tokens.nextToken();
570 					glyph.srcY = Integer.parseInt(tokens.nextToken());
571 					tokens.nextToken();
572 					glyph.width = Integer.parseInt(tokens.nextToken());
573 					tokens.nextToken();
574 					glyph.height = Integer.parseInt(tokens.nextToken());
575 					tokens.nextToken();
576 					glyph.xoffset = Integer.parseInt(tokens.nextToken());
577 					tokens.nextToken();
578 					if (flip)
579 						glyph.yoffset = Integer.parseInt(tokens.nextToken());
580 					else
581 						glyph.yoffset = -(glyph.height + Integer.parseInt(tokens.nextToken()));
582 					tokens.nextToken();
583 					glyph.xadvance = Integer.parseInt(tokens.nextToken());
584 
585 					// Check for page safely, it could be omitted or invalid.
586 					if (tokens.hasMoreTokens()) tokens.nextToken();
587 					if (tokens.hasMoreTokens()) {
588 						try {
589 							glyph.page = Integer.parseInt(tokens.nextToken());
590 						} catch (NumberFormatException ignored) {
591 						}
592 					}
593 
594 					if (glyph.width > 0 && glyph.height > 0) descent = Math.min(baseLine + glyph.yoffset, descent);
595 				}
596 				descent += padBottom;
597 
598 				while (true) {
599 					line = reader.readLine();
600 					if (line == null) break;
601 					if (!line.startsWith("kerning ")) break;
602 
603 					StringTokenizer tokens = new StringTokenizer(line, " =");
604 					tokens.nextToken();
605 					tokens.nextToken();
606 					int first = Integer.parseInt(tokens.nextToken());
607 					tokens.nextToken();
608 					int second = Integer.parseInt(tokens.nextToken());
609 					if (first < 0 || first > Character.MAX_VALUE || second < 0 || second > Character.MAX_VALUE) continue;
610 					Glyph glyph = getGlyph((char)first);
611 					tokens.nextToken();
612 					int amount = Integer.parseInt(tokens.nextToken());
613 					if (glyph != null) { // Kernings may exist for glyph pairs not contained in the font.
614 						glyph.setKerning(second, amount);
615 					}
616 				}
617 
618 				Glyph spaceGlyph = getGlyph(' ');
619 				if (spaceGlyph == null) {
620 					spaceGlyph = new Glyph();
621 					spaceGlyph.id = (int)' ';
622 					Glyph xadvanceGlyph = getGlyph('l');
623 					if (xadvanceGlyph == null) xadvanceGlyph = getFirstGlyph();
624 					spaceGlyph.xadvance = xadvanceGlyph.xadvance;
625 					setGlyph(' ', spaceGlyph);
626 				}
627 				if (spaceGlyph.width == 0) {
628 					spaceGlyph.width = (int)(padLeft + spaceGlyph.xadvance + padRight);
629 					spaceGlyph.xoffset = (int)-padLeft;
630 				}
631 				spaceWidth = spaceGlyph.width;
632 
633 				Glyph xGlyph = null;
634 				for (char xChar : xChars) {
635 					xGlyph = getGlyph(xChar);
636 					if (xGlyph != null) break;
637 				}
638 				if (xGlyph == null) xGlyph = getFirstGlyph();
639 				xHeight = xGlyph.height - padY;
640 
641 				Glyph capGlyph = null;
642 				for (char capChar : capChars) {
643 					capGlyph = getGlyph(capChar);
644 					if (capGlyph != null) break;
645 				}
646 				if (capGlyph == null) {
647 					for (Glyph[] page : this.glyphs) {
648 						if (page == null) continue;
649 						for (Glyph glyph : page) {
650 							if (glyph == null || glyph.height == 0 || glyph.width == 0) continue;
651 							capHeight = Math.max(capHeight, glyph.height);
652 						}
653 					}
654 				} else
655 					capHeight = capGlyph.height;
656 				capHeight -= padY;
657 
658 				ascent = baseLine - capHeight;
659 				down = -lineHeight;
660 				if (flip) {
661 					ascent = -ascent;
662 					down = -down;
663 				}
664 			} catch (Exception ex) {
665 				throw new GdxRuntimeException("Error loading font file: " + fontFile, ex);
666 			} finally {
667 				StreamUtils.closeQuietly(reader);
668 			}
669 		}
670 
setGlyphRegion(Glyph glyph, TextureRegion region)671 		public void setGlyphRegion (Glyph glyph, TextureRegion region) {
672 			Texture texture = region.getTexture();
673 			float invTexWidth = 1.0f / texture.getWidth();
674 			float invTexHeight = 1.0f / texture.getHeight();
675 
676 			float offsetX = 0, offsetY = 0;
677 			float u = region.u;
678 			float v = region.v;
679 			float regionWidth = region.getRegionWidth();
680 			float regionHeight = region.getRegionHeight();
681 			if (region instanceof AtlasRegion) {
682 				// Compensate for whitespace stripped from left and top edges.
683 				AtlasRegion atlasRegion = (AtlasRegion)region;
684 				offsetX = atlasRegion.offsetX;
685 				offsetY = atlasRegion.originalHeight - atlasRegion.packedHeight - atlasRegion.offsetY;
686 			}
687 
688 			float x = glyph.srcX;
689 			float x2 = glyph.srcX + glyph.width;
690 			float y = glyph.srcY;
691 			float y2 = glyph.srcY + glyph.height;
692 
693 			// Shift glyph for left and top edge stripped whitespace. Clip glyph for right and bottom edge stripped whitespace.
694 			if (offsetX > 0) {
695 				x -= offsetX;
696 				if (x < 0) {
697 					glyph.width += x;
698 					glyph.xoffset -= x;
699 					x = 0;
700 				}
701 				x2 -= offsetX;
702 				if (x2 > regionWidth) {
703 					glyph.width -= x2 - regionWidth;
704 					x2 = regionWidth;
705 				}
706 			}
707 			if (offsetY > 0) {
708 				y -= offsetY;
709 				if (y < 0) {
710 					glyph.height += y;
711 					y = 0;
712 				}
713 				y2 -= offsetY;
714 				if (y2 > regionHeight) {
715 					float amount = y2 - regionHeight;
716 					glyph.height -= amount;
717 					glyph.yoffset += amount;
718 					y2 = regionHeight;
719 				}
720 			}
721 
722 			glyph.u = u + x * invTexWidth;
723 			glyph.u2 = u + x2 * invTexWidth;
724 			if (flipped) {
725 				glyph.v = v + y * invTexHeight;
726 				glyph.v2 = v + y2 * invTexHeight;
727 			} else {
728 				glyph.v2 = v + y * invTexHeight;
729 				glyph.v = v + y2 * invTexHeight;
730 			}
731 		}
732 
733 		/** Sets the line height, which is the distance from one line of text to the next. */
setLineHeight(float height)734 		public void setLineHeight (float height) {
735 			lineHeight = height * scaleY;
736 			down = flipped ? lineHeight : -lineHeight;
737 		}
738 
setGlyph(int ch, Glyph glyph)739 		public void setGlyph (int ch, Glyph glyph) {
740 			Glyph[] page = glyphs[ch / PAGE_SIZE];
741 			if (page == null) glyphs[ch / PAGE_SIZE] = page = new Glyph[PAGE_SIZE];
742 			page[ch & PAGE_SIZE - 1] = glyph;
743 		}
744 
getFirstGlyph()745 		public Glyph getFirstGlyph () {
746 			for (Glyph[] page : this.glyphs) {
747 				if (page == null) continue;
748 				for (Glyph glyph : page) {
749 					if (glyph == null || glyph.height == 0 || glyph.width == 0) continue;
750 					return glyph;
751 				}
752 			}
753 			throw new GdxRuntimeException("No glyphs found.");
754 		}
755 
756 		/** Returns true if the font has the glyph, or if the font has a {@link #missingGlyph}. */
hasGlyph(char ch)757 		public boolean hasGlyph (char ch) {
758 			if (missingGlyph != null) return true;
759 			return getGlyph(ch) != null;
760 		}
761 
762 		/** Returns the glyph for the specified character, or null if no such glyph exists. Note that
763 		 * {@link #getGlyphs(GlyphRun, CharSequence, int, int, boolean)} should be be used to shape a string of characters into a
764 		 * list of glyphs. */
getGlyph(char ch)765 		public Glyph getGlyph (char ch) {
766 			Glyph[] page = glyphs[ch / PAGE_SIZE];
767 			if (page != null) return page[ch & PAGE_SIZE - 1];
768 			return null;
769 		}
770 
771 		/** Using the specified string, populates the glyphs and positions of the specified glyph run.
772 		 * @param str Characters to convert to glyphs. Will not contain newline or color tags. May contain "[[" for an escaped left
773 		 *           square bracket.
774 		 * @param tightBounds If true, the first {@link GlyphRun#xAdvances} entry is offset to prevent the first glyph from being
775 		 *           drawn left of 0 and the last entry is offset to prevent the last glyph from being drawn right of the run
776 		 *           width. */
getGlyphs(GlyphRun run, CharSequence str, int start, int end, boolean tightBounds)777 		public void getGlyphs (GlyphRun run, CharSequence str, int start, int end, boolean tightBounds) {
778 			boolean markupEnabled = this.markupEnabled;
779 			float scaleX = this.scaleX;
780 			Glyph missingGlyph = this.missingGlyph;
781 			Array<Glyph> glyphs = run.glyphs;
782 			FloatArray xAdvances = run.xAdvances;
783 
784 			// Guess at number of glyphs needed.
785 			glyphs.ensureCapacity(end - start);
786 			xAdvances.ensureCapacity(end - start + 1);
787 
788 			Glyph lastGlyph = null;
789 			while (start < end) {
790 				char ch = str.charAt(start++);
791 				Glyph glyph = getGlyph(ch);
792 				if (glyph == null) {
793 					if (missingGlyph == null) continue;
794 					glyph = missingGlyph;
795 				}
796 
797 				glyphs.add(glyph);
798 
799 				if (lastGlyph == null) // First glyph.
800 					xAdvances.add((!tightBounds || glyph.fixedWidth) ? 0 : -glyph.xoffset * scaleX - padLeft);
801 				else
802 					xAdvances.add((lastGlyph.xadvance + lastGlyph.getKerning(ch)) * scaleX);
803 				lastGlyph = glyph;
804 
805 				// "[[" is an escaped left square bracket, skip second character.
806 				if (markupEnabled && ch == '[' && start < end && str.charAt(start) == '[') start++;
807 			}
808 			if (lastGlyph != null) {
809 				float lastGlyphWidth = (!tightBounds || lastGlyph.fixedWidth) ? lastGlyph.xadvance
810 					: lastGlyph.xoffset + lastGlyph.width - padRight;
811 				xAdvances.add(lastGlyphWidth * scaleX);
812 			}
813 		}
814 
815 		/** Returns the first valid glyph index to use to wrap to the next line, starting at the specified start index and
816 		 * (typically) moving toward the beginning of the glyphs array. */
getWrapIndex(Array<Glyph> glyphs, int start)817 		public int getWrapIndex (Array<Glyph> glyphs, int start) {
818 			int i = start - 1;
819 			for (; i >= 1; i--)
820 				if (!isWhitespace((char)glyphs.get(i).id)) break;
821 			for (; i >= 1; i--) {
822 				char ch = (char)glyphs.get(i).id;
823 				if (isWhitespace(ch) || isBreakChar(ch)) return i + 1;
824 			}
825 			return 0;
826 		}
827 
isBreakChar(char c)828 		public boolean isBreakChar (char c) {
829 			if (breakChars == null) return false;
830 			for (char br : breakChars)
831 				if (c == br) return true;
832 			return false;
833 		}
834 
isWhitespace(char c)835 		public boolean isWhitespace (char c) {
836 			switch (c) {
837 			case '\n':
838 			case '\r':
839 			case '\t':
840 			case ' ':
841 				return true;
842 			default:
843 				return false;
844 			}
845 		}
846 
847 		/** Returns the image path for the texture page at the given index (the "id" in the BMFont file). */
getImagePath(int index)848 		public String getImagePath (int index) {
849 			return imagePaths[index];
850 		}
851 
getImagePaths()852 		public String[] getImagePaths () {
853 			return imagePaths;
854 		}
855 
getFontFile()856 		public FileHandle getFontFile () {
857 			return fontFile;
858 		}
859 
860 		/** Scales the font by the specified amounts on both axes
861 		 * <p>
862 		 * Note that smoother scaling can be achieved if the texture backing the BitmapFont is using {@link TextureFilter#Linear}.
863 		 * The default is Nearest, so use a BitmapFont constructor that takes a {@link TextureRegion}.
864 		 * @throws IllegalArgumentException if scaleX or scaleY is zero. */
setScale(float scaleX, float scaleY)865 		public void setScale (float scaleX, float scaleY) {
866 			if (scaleX == 0) throw new IllegalArgumentException("scaleX cannot be 0.");
867 			if (scaleY == 0) throw new IllegalArgumentException("scaleY cannot be 0.");
868 			float x = scaleX / this.scaleX;
869 			float y = scaleY / this.scaleY;
870 			lineHeight *= y;
871 			spaceWidth *= x;
872 			xHeight *= y;
873 			capHeight *= y;
874 			ascent *= y;
875 			descent *= y;
876 			down *= y;
877 			padTop *= y;
878 			padLeft *= y;
879 			padBottom *= y;
880 			padRight *= y;
881 			this.scaleX = scaleX;
882 			this.scaleY = scaleY;
883 		}
884 
885 		/** Scales the font by the specified amount in both directions.
886 		 * @see #setScale(float, float)
887 		 * @throws IllegalArgumentException if scaleX or scaleY is zero. */
setScale(float scaleXY)888 		public void setScale (float scaleXY) {
889 			setScale(scaleXY, scaleXY);
890 		}
891 
892 		/** Sets the font's scale relative to the current scale.
893 		 * @see #setScale(float, float)
894 		 * @throws IllegalArgumentException if the resulting scale is zero. */
scale(float amount)895 		public void scale (float amount) {
896 			setScale(scaleX + amount, scaleY + amount);
897 		}
898 	}
899 }
900