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