1 /******************************************************************************* 2 * Copyright 2011 See AUTHORS file. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 ******************************************************************************/ 16 17 package com.badlogic.gdx.graphics.g2d.freetype; 18 19 import java.io.IOException; 20 import java.io.InputStream; 21 import java.nio.ByteBuffer; 22 23 import com.badlogic.gdx.Gdx; 24 import com.badlogic.gdx.files.FileHandle; 25 import com.badlogic.gdx.graphics.Color; 26 import com.badlogic.gdx.graphics.Pixmap; 27 import com.badlogic.gdx.graphics.Pixmap.Format; 28 import com.badlogic.gdx.graphics.Texture.TextureFilter; 29 import com.badlogic.gdx.graphics.g2d.BitmapFont; 30 import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData; 31 import com.badlogic.gdx.graphics.g2d.BitmapFont.Glyph; 32 import com.badlogic.gdx.graphics.g2d.GlyphLayout.GlyphRun; 33 import com.badlogic.gdx.graphics.g2d.PixmapPacker; 34 import com.badlogic.gdx.graphics.g2d.PixmapPacker.GuillotineStrategy; 35 import com.badlogic.gdx.graphics.g2d.PixmapPacker.PackStrategy; 36 import com.badlogic.gdx.graphics.g2d.PixmapPacker.SkylineStrategy; 37 import com.badlogic.gdx.graphics.g2d.TextureRegion; 38 import com.badlogic.gdx.graphics.g2d.freetype.FreeType.Bitmap; 39 import com.badlogic.gdx.graphics.g2d.freetype.FreeType.Face; 40 import com.badlogic.gdx.graphics.g2d.freetype.FreeType.GlyphMetrics; 41 import com.badlogic.gdx.graphics.g2d.freetype.FreeType.GlyphSlot; 42 import com.badlogic.gdx.graphics.g2d.freetype.FreeType.Library; 43 import com.badlogic.gdx.graphics.g2d.freetype.FreeType.SizeMetrics; 44 import com.badlogic.gdx.graphics.g2d.freetype.FreeType.Stroker; 45 import com.badlogic.gdx.math.MathUtils; 46 import com.badlogic.gdx.math.Rectangle; 47 import com.badlogic.gdx.utils.Array; 48 import com.badlogic.gdx.utils.BufferUtils; 49 import com.badlogic.gdx.utils.Disposable; 50 import com.badlogic.gdx.utils.GdxRuntimeException; 51 import com.badlogic.gdx.utils.StreamUtils; 52 53 /** Generates {@link BitmapFont} and {@link BitmapFontData} instances from TrueType, OTF, and other FreeType supported fonts. 54 * </p> 55 * 56 * Usage example: 57 * 58 * <pre> 59 * FreeTypeFontGenerator gen = new FreeTypeFontGenerator(Gdx.files.internal("myfont.ttf")); 60 * BitmapFont font = gen.generateFont(16); 61 * gen.dispose(); 62 * </pre> 63 * 64 * The generator has to be disposed once it is no longer used. The returned {@link BitmapFont} instances are managed by the user 65 * and have to be disposed as usual. 66 * 67 * @author mzechner 68 * @author Nathan Sweet 69 * @author Rob Rendell */ 70 public class FreeTypeFontGenerator implements Disposable { 71 static public final String DEFAULT_CHARS = "\u0000ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890\"!`?'.,;:()[]{}<>|/@\\^$€-%+=#_&~*\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F\u00A0\u00A1\u00A2\u00A3\u00A4\u00A5\u00A6\u00A7\u00A8\u00A9\u00AA\u00AB\u00AC\u00AD\u00AE\u00AF\u00B0\u00B1\u00B2\u00B3\u00B4\u00B5\u00B6\u00B7\u00B8\u00B9\u00BA\u00BB\u00BC\u00BD\u00BE\u00BF\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D7\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F7\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF"; 72 73 /** A hint to scale the texture as needed, without capping it at any maximum size */ 74 static public final int NO_MAXIMUM = -1; 75 76 /** The maximum texture size allowed by generateData, when storing in a texture atlas. Multiple texture pages will be created 77 * if necessary. Default is 1024. 78 * @see #setMaxTextureSize(int) */ 79 static private int maxTextureSize = 1024; 80 81 final Library library; 82 final Face face; 83 final String name; 84 boolean bitmapped = false; 85 private int pixelWidth, pixelHeight; 86 87 /** Creates a new generator from the given font file. Uses {@link FileHandle#length()} to determine the file size. If the file 88 * length could not be determined (it was 0), an extra copy of the font bytes is performed. Throws a 89 * {@link GdxRuntimeException} if loading did not succeed. */ FreeTypeFontGenerator(FileHandle fontFile)90 public FreeTypeFontGenerator (FileHandle fontFile) { 91 name = fontFile.pathWithoutExtension(); 92 int fileSize = (int)fontFile.length(); 93 94 library = FreeType.initFreeType(); 95 if (library == null) throw new GdxRuntimeException("Couldn't initialize FreeType"); 96 97 ByteBuffer buffer; 98 InputStream input = fontFile.read(); 99 try { 100 if (fileSize == 0) { 101 // Copy to a byte[] to get the file size, then copy to the buffer. 102 byte[] data = StreamUtils.copyStreamToByteArray(input, fileSize > 0 ? (int)(fileSize * 1.5f) : 1024 * 16); 103 buffer = BufferUtils.newUnsafeByteBuffer(data.length); 104 BufferUtils.copy(data, 0, buffer, data.length); 105 } else { 106 // Trust the specified file size. 107 buffer = BufferUtils.newUnsafeByteBuffer(fileSize); 108 StreamUtils.copyStream(input, buffer); 109 } 110 } catch (IOException ex) { 111 throw new GdxRuntimeException(ex); 112 } finally { 113 StreamUtils.closeQuietly(input); 114 } 115 116 face = library.newMemoryFace(buffer, 0); 117 if (face == null) throw new GdxRuntimeException("Couldn't create face for font: " + fontFile); 118 119 if (checkForBitmapFont()) return; 120 setPixelSizes(0, 15); 121 } 122 getLoadingFlags(FreeTypeFontParameter parameter)123 private int getLoadingFlags (FreeTypeFontParameter parameter) { 124 int loadingFlags = FreeType.FT_LOAD_DEFAULT; 125 switch (parameter.hinting) { 126 case None: 127 loadingFlags |= FreeType.FT_LOAD_NO_HINTING; 128 break; 129 case Slight: 130 loadingFlags |= FreeType.FT_LOAD_FORCE_AUTOHINT | FreeType.FT_LOAD_TARGET_LIGHT; 131 break; 132 case Medium: 133 loadingFlags |= FreeType.FT_LOAD_FORCE_AUTOHINT | FreeType.FT_LOAD_TARGET_NORMAL; 134 break; 135 case Full: 136 loadingFlags |= FreeType.FT_LOAD_FORCE_AUTOHINT | FreeType.FT_LOAD_TARGET_MONO; 137 break; 138 } 139 return loadingFlags; 140 } 141 loadChar(int c)142 private boolean loadChar (int c) { 143 return loadChar(c, FreeType.FT_LOAD_DEFAULT | FreeType.FT_LOAD_FORCE_AUTOHINT); 144 } 145 loadChar(int c, int flags)146 private boolean loadChar (int c, int flags) { 147 return face.loadChar(c, flags); 148 } 149 checkForBitmapFont()150 private boolean checkForBitmapFont () { 151 int faceFlags = face.getFaceFlags(); 152 if (((faceFlags & FreeType.FT_FACE_FLAG_FIXED_SIZES) == FreeType.FT_FACE_FLAG_FIXED_SIZES) 153 && ((faceFlags & FreeType.FT_FACE_FLAG_HORIZONTAL) == FreeType.FT_FACE_FLAG_HORIZONTAL)) { 154 if (loadChar(32)) { 155 GlyphSlot slot = face.getGlyph(); 156 if (slot.getFormat() == 1651078259) { 157 bitmapped = true; 158 } 159 } 160 } 161 return bitmapped; 162 } 163 generateFont(FreeTypeFontParameter parameter)164 public BitmapFont generateFont (FreeTypeFontParameter parameter) { 165 return generateFont(parameter, new FreeTypeBitmapFontData()); 166 } 167 168 /** Generates a new {@link BitmapFont}. The size is expressed in pixels. Throws a GdxRuntimeException if the font could not be 169 * generated. Using big sizes might cause such an exception. 170 * @param parameter configures how the font is generated */ generateFont(FreeTypeFontParameter parameter, FreeTypeBitmapFontData data)171 public BitmapFont generateFont (FreeTypeFontParameter parameter, FreeTypeBitmapFontData data) { 172 generateData(parameter, data); 173 if (data.regions == null && parameter.packer != null) { 174 data.regions = new Array(); 175 parameter.packer.updateTextureRegions(data.regions, parameter.minFilter, parameter.magFilter, parameter.genMipMaps); 176 } 177 BitmapFont font = new BitmapFont(data, data.regions, true); 178 font.setOwnsTexture(parameter.packer == null); 179 return font; 180 } 181 182 /** Uses ascender and descender of font to calculate real height that makes all glyphs to fit in given pixel size. Source: 183 * http://nothings.org/stb/stb_truetype.h / stbtt_ScaleForPixelHeight */ scaleForPixelHeight(int height)184 public int scaleForPixelHeight (int height) { 185 setPixelSizes(0, height); 186 SizeMetrics fontMetrics = face.getSize().getMetrics(); 187 int ascent = FreeType.toInt(fontMetrics.getAscender()); 188 int descent = FreeType.toInt(fontMetrics.getDescender()); 189 return height * height / (ascent - descent); 190 } 191 192 /** Uses max advance, ascender and descender of font to calculate real height that makes any n glyphs to fit in given pixel 193 * width. 194 * @param width the max width to fit (in pixels) 195 * @param numChars max number of characters that to fill width */ scaleForPixelWidth(int width, int numChars)196 public int scaleForPixelWidth (int width, int numChars) { 197 SizeMetrics fontMetrics = face.getSize().getMetrics(); 198 int advance = FreeType.toInt(fontMetrics.getMaxAdvance()); 199 int ascent = FreeType.toInt(fontMetrics.getAscender()); 200 int descent = FreeType.toInt(fontMetrics.getDescender()); 201 int unscaledHeight = ascent - descent; 202 int height = unscaledHeight * width / (advance * numChars); 203 setPixelSizes(0, height); 204 return height; 205 } 206 207 /** Uses max advance, ascender and descender of font to calculate real height that makes any n glyphs to fit in given pixel 208 * width and height. 209 * @param width the max width to fit (in pixels) 210 * @param height the max height to fit (in pixels) 211 * @param numChars max number of characters that to fill width */ scaleToFitSquare(int width, int height, int numChars)212 public int scaleToFitSquare (int width, int height, int numChars) { 213 return Math.min(scaleForPixelHeight(height), scaleForPixelWidth(width, numChars)); 214 } 215 216 public class GlyphAndBitmap { 217 public Glyph glyph; 218 public Bitmap bitmap; 219 } 220 221 /** Returns null if glyph was not found. If there is nothing to render, for example with various space characters, then bitmap 222 * is null. */ generateGlyphAndBitmap(int c, int size, boolean flip)223 public GlyphAndBitmap generateGlyphAndBitmap (int c, int size, boolean flip) { 224 setPixelSizes(0, size); 225 226 SizeMetrics fontMetrics = face.getSize().getMetrics(); 227 int baseline = FreeType.toInt(fontMetrics.getAscender()); 228 229 // Check if character exists in this font. 230 // 0 means 'undefined character code' 231 if (face.getCharIndex(c) == 0) { 232 return null; 233 } 234 235 // Try to load character 236 if (!loadChar(c)) { 237 throw new GdxRuntimeException("Unable to load character!"); 238 } 239 240 GlyphSlot slot = face.getGlyph(); 241 242 // Try to render to bitmap 243 Bitmap bitmap; 244 if (bitmapped) { 245 bitmap = slot.getBitmap(); 246 } else if (!slot.renderGlyph(FreeType.FT_RENDER_MODE_NORMAL)) { 247 bitmap = null; 248 } else { 249 bitmap = slot.getBitmap(); 250 } 251 252 GlyphMetrics metrics = slot.getMetrics(); 253 254 Glyph glyph = new Glyph(); 255 if (bitmap != null) { 256 glyph.width = bitmap.getWidth(); 257 glyph.height = bitmap.getRows(); 258 } else { 259 glyph.width = 0; 260 glyph.height = 0; 261 } 262 glyph.xoffset = slot.getBitmapLeft(); 263 glyph.yoffset = flip ? -slot.getBitmapTop() + baseline : -(glyph.height - slot.getBitmapTop()) - baseline; 264 glyph.xadvance = FreeType.toInt(metrics.getHoriAdvance()); 265 glyph.srcX = 0; 266 glyph.srcY = 0; 267 glyph.id = c; 268 269 GlyphAndBitmap result = new GlyphAndBitmap(); 270 result.glyph = glyph; 271 result.bitmap = bitmap; 272 return result; 273 } 274 275 /** Generates a new {@link BitmapFontData} instance, expert usage only. Throws a GdxRuntimeException if something went wrong. 276 * @param size the size in pixels */ generateData(int size)277 public FreeTypeBitmapFontData generateData (int size) { 278 FreeTypeFontParameter parameter = new FreeTypeFontParameter(); 279 parameter.size = size; 280 return generateData(parameter); 281 } 282 generateData(FreeTypeFontParameter parameter)283 public FreeTypeBitmapFontData generateData (FreeTypeFontParameter parameter) { 284 return generateData(parameter, new FreeTypeBitmapFontData()); 285 } 286 setPixelSizes(int pixelWidth, int pixelHeight)287 void setPixelSizes (int pixelWidth, int pixelHeight) { 288 this.pixelWidth = pixelWidth; 289 this.pixelHeight = pixelHeight; 290 if (!bitmapped && !face.setPixelSizes(pixelWidth, pixelHeight)) throw new GdxRuntimeException("Couldn't set size for font"); 291 } 292 293 /** Generates a new {@link BitmapFontData} instance, expert usage only. Throws a GdxRuntimeException if something went wrong. 294 * @param parameter configures how the font is generated */ generateData(FreeTypeFontParameter parameter, FreeTypeBitmapFontData data)295 public FreeTypeBitmapFontData generateData (FreeTypeFontParameter parameter, FreeTypeBitmapFontData data) { 296 parameter = parameter == null ? new FreeTypeFontParameter() : parameter; 297 char[] characters = parameter.characters.toCharArray(); 298 int charactersLength = characters.length; 299 boolean incremental = parameter.incremental; 300 int flags = getLoadingFlags(parameter); 301 302 setPixelSizes(0, parameter.size); 303 304 // set general font data 305 SizeMetrics fontMetrics = face.getSize().getMetrics(); 306 data.flipped = parameter.flip; 307 data.ascent = FreeType.toInt(fontMetrics.getAscender()); 308 data.descent = FreeType.toInt(fontMetrics.getDescender()); 309 data.lineHeight = FreeType.toInt(fontMetrics.getHeight()); 310 float baseLine = data.ascent; 311 312 // if bitmapped 313 if (bitmapped && (data.lineHeight == 0)) { 314 for (int c = 32; c < (32 + face.getNumGlyphs()); c++) { 315 if (loadChar(c, flags)) { 316 int lh = FreeType.toInt(face.getGlyph().getMetrics().getHeight()); 317 data.lineHeight = (lh > data.lineHeight) ? lh : data.lineHeight; 318 } 319 } 320 } 321 data.lineHeight += parameter.spaceY; 322 323 // determine space width 324 if (loadChar(' ', flags) || loadChar('l', flags)) { 325 data.spaceWidth = FreeType.toInt(face.getGlyph().getMetrics().getHoriAdvance()); 326 } else { 327 data.spaceWidth = face.getMaxAdvanceWidth(); // Possibly very wrong. 328 } 329 330 // determine x-height 331 for (char xChar : data.xChars) { 332 if (!loadChar(xChar, flags)) continue; 333 data.xHeight = FreeType.toInt(face.getGlyph().getMetrics().getHeight()); 334 break; 335 } 336 if (data.xHeight == 0) throw new GdxRuntimeException("No x-height character found in font"); 337 338 // determine cap height 339 for (char capChar : data.capChars) { 340 if (!loadChar(capChar, flags)) continue; 341 data.capHeight = FreeType.toInt(face.getGlyph().getMetrics().getHeight()); 342 break; 343 } 344 if (!bitmapped && data.capHeight == 1) throw new GdxRuntimeException("No cap character found in font"); 345 346 data.ascent -= data.capHeight; 347 data.down = -data.lineHeight; 348 if (parameter.flip) { 349 data.ascent = -data.ascent; 350 data.down = -data.down; 351 } 352 353 boolean ownsAtlas = false; 354 355 PixmapPacker packer = parameter.packer; 356 357 if (packer == null) { 358 // Create a packer. 359 int size; 360 PackStrategy packStrategy; 361 if (incremental) { 362 size = maxTextureSize; 363 packStrategy = new GuillotineStrategy(); 364 } else { 365 int maxGlyphHeight = (int)Math.ceil(data.lineHeight); 366 size = MathUtils.nextPowerOfTwo((int)Math.sqrt(maxGlyphHeight * maxGlyphHeight * charactersLength)); 367 if (maxTextureSize > 0) size = Math.min(size, maxTextureSize); 368 packStrategy = new SkylineStrategy(); 369 } 370 ownsAtlas = true; 371 packer = new PixmapPacker(size, size, Format.RGBA8888, 1, false, packStrategy); 372 packer.setTransparentColor(parameter.color); 373 packer.getTransparentColor().a = 0; 374 if (parameter.borderWidth > 0) { 375 packer.setTransparentColor(parameter.borderColor); 376 packer.getTransparentColor().a = 0; 377 } 378 } 379 380 if (incremental) data.glyphs = new Array(charactersLength + 32); 381 382 Stroker stroker = null; 383 if (parameter.borderWidth > 0) { 384 stroker = library.createStroker(); 385 stroker.set((int)(parameter.borderWidth * 64f), 386 parameter.borderStraight ? FreeType.FT_STROKER_LINECAP_BUTT : FreeType.FT_STROKER_LINECAP_ROUND, 387 parameter.borderStraight ? FreeType.FT_STROKER_LINEJOIN_MITER_FIXED : FreeType.FT_STROKER_LINEJOIN_ROUND, 0); 388 } 389 390 Glyph missingGlyph = createGlyph('\0', data, parameter, stroker, baseLine, packer); 391 if (missingGlyph != null && missingGlyph.width != 0 && missingGlyph.height != 0) { 392 data.setGlyph('\0', missingGlyph); 393 if (incremental) data.glyphs.add(missingGlyph); 394 } 395 396 // Create glyphs largest height first for best packing. 397 int[] heights = new int[charactersLength]; 398 for (int i = 0, n = charactersLength; i < n; i++) { 399 int height = loadChar(characters[i], flags) ? FreeType.toInt(face.getGlyph().getMetrics().getHeight()) : 0; 400 heights[i] = height; 401 } 402 int heightsCount = heights.length; 403 while (heightsCount > 0) { 404 int best = 0, maxHeight = heights[0]; 405 for (int i = 1; i < heightsCount; i++) { 406 int height = heights[i]; 407 if (height > maxHeight) { 408 maxHeight = height; 409 best = i; 410 } 411 } 412 413 char c = characters[best]; 414 Glyph glyph = createGlyph(c, data, parameter, stroker, baseLine, packer); 415 if (glyph != null) { 416 data.setGlyph(c, glyph); 417 if (incremental) data.glyphs.add(glyph); 418 } 419 420 heightsCount--; 421 heights[best] = heights[heightsCount]; 422 char tmpChar = characters[best]; 423 characters[best] = characters[heightsCount]; 424 characters[heightsCount] = tmpChar; 425 } 426 427 if (stroker != null && !incremental) stroker.dispose(); 428 429 if (incremental) { 430 data.generator = this; 431 data.parameter = parameter; 432 data.stroker = stroker; 433 data.packer = packer; 434 } 435 436 // Generate kerning. 437 parameter.kerning &= face.hasKerning(); 438 if (parameter.kerning) { 439 for (int i = 0; i < charactersLength; i++) { 440 char firstChar = characters[i]; 441 Glyph first = data.getGlyph(firstChar); 442 if (first == null) continue; 443 int firstIndex = face.getCharIndex(firstChar); 444 for (int ii = i; ii < charactersLength; ii++) { 445 char secondChar = characters[ii]; 446 Glyph second = data.getGlyph(secondChar); 447 if (second == null) continue; 448 int secondIndex = face.getCharIndex(secondChar); 449 450 int kerning = face.getKerning(firstIndex, secondIndex, 0); 451 if (kerning != 0) first.setKerning(secondChar, FreeType.toInt(kerning)); 452 453 kerning = face.getKerning(secondIndex, firstIndex, 0); 454 if (kerning != 0) second.setKerning(firstChar, FreeType.toInt(kerning)); 455 } 456 } 457 } 458 459 // Generate texture regions. 460 if (ownsAtlas) { 461 data.regions = new Array(); 462 packer.updateTextureRegions(data.regions, parameter.minFilter, parameter.magFilter, parameter.genMipMaps); 463 } 464 465 // Set space glyph. 466 Glyph spaceGlyph = data.getGlyph(' '); 467 if (spaceGlyph == null) { 468 spaceGlyph = new Glyph(); 469 spaceGlyph.xadvance = (int)data.spaceWidth + parameter.spaceX; 470 spaceGlyph.id = (int)' '; 471 data.setGlyph(' ', spaceGlyph); 472 } 473 if (spaceGlyph.width == 0) spaceGlyph.width = (int)(spaceGlyph.xadvance + data.padRight); 474 475 return data; 476 } 477 478 /** @return null if glyph was not found. */ createGlyph(char c, FreeTypeBitmapFontData data, FreeTypeFontParameter parameter, Stroker stroker, float baseLine, PixmapPacker packer)479 Glyph createGlyph (char c, FreeTypeBitmapFontData data, FreeTypeFontParameter parameter, Stroker stroker, float baseLine, 480 PixmapPacker packer) { 481 482 boolean missing = face.getCharIndex(c) == 0 && c != 0; 483 if (missing) return null; 484 485 if (!loadChar(c, getLoadingFlags(parameter))) return null; 486 487 GlyphSlot slot = face.getGlyph(); 488 FreeType.Glyph mainGlyph = slot.getGlyph(); 489 try { 490 mainGlyph.toBitmap(parameter.mono ? FreeType.FT_RENDER_MODE_MONO : FreeType.FT_RENDER_MODE_NORMAL); 491 } catch (GdxRuntimeException e) { 492 mainGlyph.dispose(); 493 Gdx.app.log("FreeTypeFontGenerator", "Couldn't render char: " + c); 494 return null; 495 } 496 Bitmap mainBitmap = mainGlyph.getBitmap(); 497 Pixmap mainPixmap = mainBitmap.getPixmap(Format.RGBA8888, parameter.color, parameter.gamma); 498 499 if (mainBitmap.getWidth() != 0 && mainBitmap.getRows() != 0) { 500 int offsetX = 0, offsetY = 0; 501 if (parameter.borderWidth > 0) { 502 // execute stroker; this generates a glyph "extended" along the outline 503 int top = mainGlyph.getTop(), left = mainGlyph.getLeft(); 504 FreeType.Glyph borderGlyph = slot.getGlyph(); 505 borderGlyph.strokeBorder(stroker, false); 506 borderGlyph.toBitmap(parameter.mono ? FreeType.FT_RENDER_MODE_MONO : FreeType.FT_RENDER_MODE_NORMAL); 507 offsetX = left - borderGlyph.getLeft(); 508 offsetY = -(top - borderGlyph.getTop()); 509 510 // Render border (pixmap is bigger than main). 511 Bitmap borderBitmap = borderGlyph.getBitmap(); 512 Pixmap borderPixmap = borderBitmap.getPixmap(Format.RGBA8888, parameter.borderColor, parameter.borderGamma); 513 514 // Draw main glyph on top of border. 515 for (int i = 0, n = parameter.renderCount; i < n; i++) 516 borderPixmap.drawPixmap(mainPixmap, offsetX, offsetY); 517 518 mainPixmap.dispose(); 519 mainGlyph.dispose(); 520 mainPixmap = borderPixmap; 521 mainGlyph = borderGlyph; 522 } 523 524 if (parameter.shadowOffsetX != 0 || parameter.shadowOffsetY != 0) { 525 int mainW = mainPixmap.getWidth(), mainH = mainPixmap.getHeight(); 526 int shadowOffsetX = Math.max(parameter.shadowOffsetX, 0), shadowOffsetY = Math.max(parameter.shadowOffsetY, 0); 527 int shadowW = mainW + Math.abs(parameter.shadowOffsetX), shadowH = mainH + Math.abs(parameter.shadowOffsetY); 528 Pixmap shadowPixmap = new Pixmap(shadowW, shadowH, mainPixmap.getFormat()); 529 530 Color shadowColor = parameter.shadowColor; 531 byte r = (byte)(shadowColor.r * 255), g = (byte)(shadowColor.g * 255), b = (byte)(shadowColor.b * 255); 532 float a = shadowColor.a; 533 534 ByteBuffer mainPixels = mainPixmap.getPixels(); 535 ByteBuffer shadowPixels = shadowPixmap.getPixels(); 536 for (int y = 0; y < mainH; y++) { 537 int shadowRow = shadowW * (y + shadowOffsetY) + shadowOffsetX; 538 for (int x = 0; x < mainW; x++) { 539 int mainPixel = (mainW * y + x) * 4; 540 byte mainA = mainPixels.get(mainPixel + 3); 541 if (mainA == 0) continue; 542 int shadowPixel = (shadowRow + x) * 4; 543 shadowPixels.put(shadowPixel, r); 544 shadowPixels.put(shadowPixel + 1, g); 545 shadowPixels.put(shadowPixel + 2, b); 546 shadowPixels.put(shadowPixel + 3, (byte)((mainA & 0xff) * a)); 547 } 548 } 549 550 // Draw main glyph (with any border) on top of shadow. 551 for (int i = 0, n = parameter.renderCount; i < n; i++) 552 shadowPixmap.drawPixmap(mainPixmap, Math.max(-parameter.shadowOffsetX, 0), Math.max(-parameter.shadowOffsetY, 0)); 553 mainPixmap.dispose(); 554 mainPixmap = shadowPixmap; 555 } else if (parameter.borderWidth == 0) { 556 // No shadow and no border, draw glyph additional times. 557 for (int i = 0, n = parameter.renderCount - 1; i < n; i++) 558 mainPixmap.drawPixmap(mainPixmap, 0, 0); 559 } 560 } 561 562 GlyphMetrics metrics = slot.getMetrics(); 563 Glyph glyph = new Glyph(); 564 glyph.id = c; 565 glyph.width = mainPixmap.getWidth(); 566 glyph.height = mainPixmap.getHeight(); 567 glyph.xoffset = mainGlyph.getLeft(); 568 glyph.yoffset = parameter.flip ? -mainGlyph.getTop() + (int)baseLine : -(glyph.height - mainGlyph.getTop()) - (int)baseLine; 569 glyph.xadvance = FreeType.toInt(metrics.getHoriAdvance()) + (int)parameter.borderWidth + parameter.spaceX; 570 571 if (bitmapped) { 572 mainPixmap.setColor(Color.CLEAR); 573 mainPixmap.fill(); 574 ByteBuffer buf = mainBitmap.getBuffer(); 575 int whiteIntBits = Color.WHITE.toIntBits(); 576 int clearIntBits = Color.CLEAR.toIntBits(); 577 for (int h = 0; h < glyph.height; h++) { 578 int idx = h * mainBitmap.getPitch(); 579 for (int w = 0; w < (glyph.width + glyph.xoffset); w++) { 580 int bit = (buf.get(idx + (w / 8)) >>> (7 - (w % 8))) & 1; 581 mainPixmap.drawPixel(w, h, ((bit == 1) ? whiteIntBits : clearIntBits)); 582 } 583 } 584 585 } 586 587 Rectangle rect = packer.pack(mainPixmap); 588 glyph.page = packer.getPages().size - 1; // Glyph is always packed into the last page for now. 589 glyph.srcX = (int)rect.x; 590 glyph.srcY = (int)rect.y; 591 592 // If a page was added, create a new texture region for the incrementally added glyph. 593 if (parameter.incremental && data.regions != null && data.regions.size <= glyph.page) 594 packer.updateTextureRegions(data.regions, parameter.minFilter, parameter.magFilter, parameter.genMipMaps); 595 596 mainPixmap.dispose(); 597 mainGlyph.dispose(); 598 599 return glyph; 600 } 601 602 /** Cleans up all resources of the generator. Call this if you no longer use the generator. */ 603 @Override dispose()604 public void dispose () { 605 face.dispose(); 606 library.dispose(); 607 } 608 609 /** Sets the maximum size that will be used when generating texture atlases for glyphs with <tt>generateData()</tt>. The 610 * default is 1024. By specifying {@link #NO_MAXIMUM}, the texture atlas will scale as needed. 611 * 612 * The power-of-two square texture size will be capped to the given <tt>texSize</tt>. It's recommended that a power-of-two 613 * value be used here. 614 * 615 * Multiple pages may be used to fit all the generated glyphs. You can query the resulting number of pages by calling 616 * <tt>bitmapFont.getRegions().length</tt> or <tt>freeTypeBitmapFontData.getTextureRegions().length</tt>. 617 * 618 * If PixmapPacker is specified when calling generateData, this parameter is ignored. 619 * 620 * @param texSize the maximum texture size for one page of glyphs */ setMaxTextureSize(int texSize)621 public static void setMaxTextureSize (int texSize) { 622 maxTextureSize = texSize; 623 } 624 625 /** Returns the maximum texture size that will be used by generateData() when creating a texture atlas for the glyphs. 626 * @return the power-of-two max texture size */ getMaxTextureSize()627 public static int getMaxTextureSize () { 628 return maxTextureSize; 629 } 630 631 /** {@link BitmapFontData} used for fonts generated via the {@link FreeTypeFontGenerator}. The texture storing the glyphs is 632 * held in memory, thus the {@link #getImagePaths()} and {@link #getFontFile()} methods will return null. 633 * @author mzechner 634 * @author Nathan Sweet */ 635 static public class FreeTypeBitmapFontData extends BitmapFontData implements Disposable { 636 Array<TextureRegion> regions; 637 638 // Fields for incremental glyph generation. 639 FreeTypeFontGenerator generator; 640 FreeTypeFontParameter parameter; 641 Stroker stroker; 642 PixmapPacker packer; 643 Array<Glyph> glyphs; 644 private boolean dirty; 645 646 @Override getGlyph(char ch)647 public Glyph getGlyph (char ch) { 648 Glyph glyph = super.getGlyph(ch); 649 if (glyph == null && generator != null) { 650 generator.setPixelSizes(0, parameter.size); 651 float baseline = ((flipped ? -ascent : ascent) + capHeight) / scaleY; 652 glyph = generator.createGlyph(ch, this, parameter, stroker, baseline, packer); 653 if (glyph == null) return missingGlyph; 654 655 setGlyphRegion(glyph, regions.get(glyph.page)); 656 setGlyph(ch, glyph); 657 glyphs.add(glyph); 658 dirty = true; 659 660 Face face = generator.face; 661 if (parameter.kerning) { 662 int glyphIndex = face.getCharIndex(ch); 663 for (int i = 0, n = glyphs.size; i < n; i++) { 664 Glyph other = glyphs.get(i); 665 int otherIndex = face.getCharIndex(other.id); 666 667 int kerning = face.getKerning(glyphIndex, otherIndex, 0); 668 if (kerning != 0) glyph.setKerning(other.id, FreeType.toInt(kerning)); 669 670 kerning = face.getKerning(otherIndex, glyphIndex, 0); 671 if (kerning != 0) other.setKerning(ch, FreeType.toInt(kerning)); 672 } 673 } 674 } 675 return glyph; 676 } 677 getGlyphs(GlyphRun run, CharSequence str, int start, int end, boolean tightBounds)678 public void getGlyphs (GlyphRun run, CharSequence str, int start, int end, boolean tightBounds) { 679 if (packer != null) packer.setPackToTexture(true); // All glyphs added after this are packed directly to the texture. 680 super.getGlyphs(run, str, start, end, tightBounds); 681 if (dirty) { 682 dirty = false; 683 packer.updateTextureRegions(regions, parameter.minFilter, parameter.magFilter, parameter.genMipMaps); 684 } 685 } 686 687 @Override dispose()688 public void dispose () { 689 if (stroker != null) stroker.dispose(); 690 if (packer != null) packer.dispose(); 691 } 692 } 693 694 /** Font smoothing algorithm. */ 695 public static enum Hinting { 696 /** Disable hinting. Generated glyphs will look blurry. */ 697 None, 698 /** Light hinting with fuzzy edges, but close to the original shape */ 699 Slight, 700 /** Default hinting */ 701 Medium, 702 /** Strong hinting with crisp edges at the expense of shape fidelity */ 703 Full 704 } 705 706 /** Parameter container class that helps configure how {@link FreeTypeBitmapFontData} and {@link BitmapFont} instances are 707 * generated. 708 * 709 * The packer field is for advanced usage, where it is necessary to pack multiple BitmapFonts (i.e. styles, sizes, families) 710 * into a single Texture atlas. If no packer is specified, the generator will use its own PixmapPacker to pack the glyphs into 711 * a power-of-two sized texture, and the resulting {@link FreeTypeBitmapFontData} will have a valid {@link TextureRegion} which 712 * can be used to construct a new {@link BitmapFont}. 713 * 714 * @author siondream 715 * @author Nathan Sweet */ 716 public static class FreeTypeFontParameter { 717 /** The size in pixels */ 718 public int size = 16; 719 /** If true, font smoothing is disabled. */ 720 public boolean mono; 721 /** Strength of hinting when smoothing is enabled */ 722 public Hinting hinting = Hinting.Medium; 723 /** Foreground color (required for non-black borders) */ 724 public Color color = Color.WHITE; 725 /** Glyph gamma. Values > 1 reduce antialiasing. */ 726 public float gamma = 1.8f; 727 /** Number of times to render the glyph. Useful with a shadow or border, so it doesn't show through the glyph. */ 728 public int renderCount = 2; 729 /** Border width in pixels, 0 to disable */ 730 public float borderWidth = 0; 731 /** Border color; only used if borderWidth > 0 */ 732 public Color borderColor = Color.BLACK; 733 /** true for straight (mitered), false for rounded borders */ 734 public boolean borderStraight = false; 735 /** Values < 1 increase the border size. */ 736 public float borderGamma = 1.8f; 737 /** Offset of text shadow on X axis in pixels, 0 to disable */ 738 public int shadowOffsetX = 0; 739 /** Offset of text shadow on Y axis in pixels, 0 to disable */ 740 public int shadowOffsetY = 0; 741 /** Shadow color; only used if shadowOffset > 0 */ 742 public Color shadowColor = new Color(0, 0, 0, 0.75f); 743 /** Pixels to add to glyph spacing. Can be negative. */ 744 public int spaceX, spaceY; 745 /** The characters the font should contain */ 746 public String characters = DEFAULT_CHARS; 747 /** Whether the font should include kerning */ 748 public boolean kerning = true; 749 /** The optional PixmapPacker to use */ 750 public PixmapPacker packer = null; 751 /** Whether to flip the font vertically */ 752 public boolean flip = false; 753 /** Whether to generate mip maps for the resulting texture */ 754 public boolean genMipMaps = false; 755 /** Minification filter */ 756 public TextureFilter minFilter = TextureFilter.Nearest; 757 /** Magnification filter */ 758 public TextureFilter magFilter = TextureFilter.Nearest; 759 /** When true, glyphs are rendered on the fly to the font's glyph page textures as they are needed. The 760 * FreeTypeFontGenerator must not be disposed until the font is no longer needed. The FreeTypeBitmapFontData must be 761 * disposed (separately from the generator) when the font is no longer needed. The FreeTypeFontParameter should not be 762 * modified after creating a font. If a PixmapPacker is not specified, the font glyph page textures will use 763 * {@link FreeTypeFontGenerator#getMaxTextureSize()}. */ 764 public boolean incremental; 765 } 766 } 767