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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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 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) */ 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) */ 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) */ 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) */ 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) */ 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. */ 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()}. */ 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()}. */ 243 public void setColor (float r, float g, float b, float a) { 244 cache.getColor().set(r, g, b, a); 245 } 246 247 public float getScaleX () { 248 return data.scaleX; 249 } 250 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 */ 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 */ 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 */ 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. */ 275 public float getLineHeight () { 276 return data.lineHeight; 277 } 278 279 /** Returns the width of the space character. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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 */ 352 public BitmapFontCache getCache () { 353 return cache; 354 } 355 356 /** Gets the underlying {@link BitmapFontData} for this BitmapFont. */ 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 */ 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 */ 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. */ 378 public BitmapFontCache newFontCache () { 379 return new BitmapFontCache(this, integer); 380 } 381 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 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 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 417 public String toString () { 418 return Character.toString((char)id); 419 } 420 } 421 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. */ 469 public BitmapFontData () { 470 } 471 472 public BitmapFontData (FileHandle fontFile, boolean flip) { 473 this.fontFile = fontFile; 474 this.flipped = flip; 475 load(fontFile, flip); 476 } 477 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 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. */ 734 public void setLineHeight (float height) { 735 lineHeight = height * scaleY; 736 down = flipped ? lineHeight : -lineHeight; 737 } 738 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 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}. */ 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. */ 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. */ 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. */ 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 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 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). */ 848 public String getImagePath (int index) { 849 return imagePaths[index]; 850 } 851 852 public String[] getImagePaths () { 853 return imagePaths; 854 } 855 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. */ 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. */ 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. */ 895 public void scale (float amount) { 896 setScale(scaleX + amount, scaleY + amount); 897 } 898 } 899 } 900