Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Created May 3, 2013 18:12
Show Gist options
  • Save mattdesl/5512218 to your computer and use it in GitHub Desktop.
Save mattdesl/5512218 to your computer and use it in GitHub Desktop.
// Of course there are better ways to abstract this process for many styles/sizes.
// Here is just a brief example without any abstraction.
////// 1. Use PixmapPacker for tight bin packing.. we can adjust spacing/size
PixmapPacker atlas = new PixmapPacker(512, 512, Format.RGBA8888, 2, false);
//generate regular fonts
//prefix string doesn't really matter as long as its unique
TTFGenerator ttf = new TTFGenerator(Gdx.files.internal("data/Roboto-Regular.ttf"));
FreeTypeBitmapFontData d_reg16 = ttf.generateData(16, TTFGenerator.DEFAULT_CHARS, false, atlas, "16_");
FreeTypeBitmapFontData d_reg18 = ttf.generateData(18, TTFGenerator.DEFAULT_CHARS, false, atlas, "18_");
ttf.dispose();
//generate bold fonts
ttf = new TTFGenerator(Gdx.files.internal("data/Roboto-Bold.ttf"));
FreeTypeBitmapFontData d_bold16 = ttf.generateData(16, TTFGenerator.DEFAULT_CHARS, false, atlas, "bold_16_");
FreeTypeBitmapFontData d_bold18 = ttf.generateData(18, TTFGenerator.DEFAULT_CHARS, false, atlas, "bold_18_");
ttf.dispose();
////// 2. Upload pixel data to GPU
//get first page... this only works with 1 page so far
Page p = atlas.getPages().get(0);
//generate texture from pixmap packer
Texture tex = new Texture(p.getPixmap(), p.getPixmap().getFormat(), false);
tex.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
//dispose pixmap packer
atlas.dispose();
//texture region for our data
TextureRegion region = new TextureRegion(tex);
d_reg16.region = region;
d_reg18.region = region;
d_bold16.region = region;
d_bold18.region = region;
////// 3. Create fonts
BitmapFont reg16 = new BitmapFont(d_reg16, region, false);
BitmapFont reg18 = new BitmapFont(d_reg18, region, false);
BitmapFont bold16 = new BitmapFont(d_bold16, region, false);
BitmapFont bold18 = new BitmapFont(d_bold18, region, false);
package mdesl.line2dx.test;
/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData;
import com.badlogic.gdx.graphics.g2d.BitmapFont.Glyph;
import com.badlogic.gdx.graphics.g2d.PixmapPacker;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.g2d.freetype.FreeType;
import com.badlogic.gdx.graphics.g2d.freetype.FreeType.Bitmap;
import com.badlogic.gdx.graphics.g2d.freetype.FreeType.Face;
import com.badlogic.gdx.graphics.g2d.freetype.FreeType.GlyphMetrics;
import com.badlogic.gdx.graphics.g2d.freetype.FreeType.GlyphSlot;
import com.badlogic.gdx.graphics.g2d.freetype.FreeType.Library;
import com.badlogic.gdx.graphics.g2d.freetype.FreeType.SizeMetrics;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;
/** Generates {@link BitmapFont} and {@link BitmapFontData} instances from TrueType font files.</p>
*
* Usage example:
*
* <pre>
* FreeTypeFontGenerator gen = new FreeTypeFontGenerator(Gdx.files.internal(&quot;myfont.ttf&quot;));
* BitmapFont font = gen.generateFont(16);
* gen.dispose();
* </pre>
*
* The generator has to be disposed once it is no longer used. The returned {@link BitmapFont} instances are managed by the user
* and have to be disposed as usual.
*
* @author mzechner */
public class TTFGenerator implements Disposable {
public static final String DEFAULT_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890\"!`?'.,;:()[]{}<>|/@\\^$-%+=#_&~*\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";
final Library library;
final Face face;
/** Creates a new generator from the given TrueType font file. Throws a {@link GdxRuntimeException} in case loading did not
* succeed.
* @param font the {@link FileHandle} to the TrueType font file */
public TTFGenerator (FileHandle font) {
library = FreeType.initFreeType();
if (library == null) throw new GdxRuntimeException("Couldn't initialize FreeType");
face = FreeType.newFace(library, font, 0);
if (face == null) throw new GdxRuntimeException("Couldn't create face for font '" + font + "'");
if (!FreeType.setPixelSizes(face, 0, 15)) throw new GdxRuntimeException("Couldn't set size for font '" + font + "'");
}
/** Generates a new {@link BitmapFont}, containing glyphs for the given characters. The size is expressed in pixels. Throws a
* GdxRuntimeException in case the font could not be generated. Using big sizes might cause such an exception. All characters
* need to fit onto a single texture.
* @param size the size in pixels
* @param characters the characters the font should contain
* @param flip whether to flip the font horizontally, see {@link BitmapFont#BitmapFont(FileHandle, TextureRegion, boolean)} */
public BitmapFont generateFont (int size, String characters, boolean flip) {
FreeTypeBitmapFontData data = generateData(size, characters, flip, null, null);
BitmapFont font = new BitmapFont(data, data.getTextureRegion(), false);
font.setOwnsTexture(true);
return font;
}
/** Generates a new {@link BitmapFont}. The size is expressed in pixels. Throws a GdxRuntimeException in case the font could not
* be generated. Using big sizes might cause such an exception. All characters need to fit onto a single texture.
*
* @param size the size of the font in pixels */
public BitmapFont generateFont (int size) {
return generateFont(size, DEFAULT_CHARS, false);
}
/** Uses ascender and descender of font to calculate real height that makes
* all glyphs to fit in given pixel size. Source:
* http://nothings.org/stb/stb_truetype.h / stbtt_ScaleForPixelHeight
*/
public int scaleForPixelHeight(int size) {
if (!FreeType.setPixelSizes(face, 0, size)) throw new GdxRuntimeException("Couldn't set size for font");
SizeMetrics fontMetrics = face.getSize().getMetrics();
int ascent = FreeType.toInt(fontMetrics.getAscender());
int descent = FreeType.toInt(fontMetrics.getDescender());
return size * size / (ascent - descent);
}
public class GlyphAndBitmap {
public Glyph glyph;
public Bitmap bitmap;
}
/** Returns null if glyph was not found. If there is nothing to render,
* for example with various space characters, then bitmap is null.
* */
public GlyphAndBitmap generateGlyphAndBitmap(int c, int size, boolean flip) {
if (!FreeType.setPixelSizes(face, 0, size)) throw new GdxRuntimeException("Couldn't set size for font");
SizeMetrics fontMetrics = face.getSize().getMetrics();
int baseline = FreeType.toInt(fontMetrics.getAscender());
// Check if character exists in this font.
// 0 means 'undefined character code'
if (FreeType.getCharIndex(face, c) == 0) {
return null;
}
// Try to load character
if (!FreeType.loadChar(face, c, FreeType.FT_LOAD_DEFAULT)) {
throw new GdxRuntimeException("Unable to load character!");
}
GlyphSlot slot = face.getGlyph();
// Try to render to bitmap
Bitmap bitmap;
if (!FreeType.renderGlyph(slot, FreeType.FT_RENDER_MODE_LIGHT)) {
bitmap = null;
} else {
bitmap = slot.getBitmap();
}
GlyphMetrics metrics = slot.getMetrics();
Glyph glyph = new Glyph();
if (bitmap != null) {
glyph.width = bitmap.getWidth();
glyph.height = bitmap.getRows();
} else {
glyph.width = 0;
glyph.height = 0;
}
glyph.xoffset = slot.getBitmapLeft();
glyph.yoffset = flip ? -slot.getBitmapTop() + baseline : -(glyph.height - slot.getBitmapTop()) - baseline;
glyph.xadvance = FreeType.toInt(metrics.getHoriAdvance());
glyph.srcX = 0;
glyph.srcY = 0;
GlyphAndBitmap result = new GlyphAndBitmap();
result.glyph = glyph;
result.bitmap = bitmap;
return result;
}
/** Generates a new {@link BitmapFontData} instance, expert usage only. Throws a GdxRuntimeException in case something went
* wrong.
* @param size the size in pixels */
public FreeTypeBitmapFontData generateData (int size) {
return generateData(size, DEFAULT_CHARS, false, null, null);
}
public static int count;
/** Generates a new {@link BitmapFontData} instance, expert usage only. Throws a GdxRuntimeException in case something went
* wrong.
*
* @param size the size in pixels
* @param characters the characters the font should contain
* @param flip whether to flip the font horizontally, see {@link BitmapFont#BitmapFont(FileHandle, TextureRegion, boolean)} */
public FreeTypeBitmapFontData generateData (int size, String characters, boolean flip, PixmapPacker atlas, String packPrefix) {
FreeTypeBitmapFontData data = new FreeTypeBitmapFontData();
if (!FreeType.setPixelSizes(face, 0, size)) throw new GdxRuntimeException("Couldn't set size for font");
// set general font data
SizeMetrics fontMetrics = face.getSize().getMetrics();
data.flipped = flip;
data.ascent = FreeType.toInt(fontMetrics.getAscender());
data.descent = FreeType.toInt(fontMetrics.getDescender());
data.lineHeight = FreeType.toInt(fontMetrics.getHeight());
float baseLine = data.ascent;
// determine space width and set glyph
if (FreeType.loadChar(face, ' ', FreeType.FT_LOAD_DEFAULT)) {
data.spaceWidth = FreeType.toInt(face.getGlyph().getMetrics().getHoriAdvance());
} else {
data.spaceWidth = face.getMaxAdvanceWidth(); // FIXME possibly very wrong :)
}
Glyph spaceGlyph = new Glyph();
spaceGlyph.xadvance = (int)data.spaceWidth;
data.setGlyph(' ', spaceGlyph);
// determine x-height
for (char xChar : BitmapFont.xChars) {
if (!FreeType.loadChar(face, xChar, FreeType.FT_LOAD_DEFAULT)) continue;
data.xHeight = FreeType.toInt(face.getGlyph().getMetrics().getHeight());
break;
}
if (data.xHeight == 0) throw new GdxRuntimeException("No x-height character found in font");
for (char capChar : BitmapFont.capChars) {
if (!FreeType.loadChar(face, capChar, FreeType.FT_LOAD_DEFAULT)) continue;
data.capHeight = FreeType.toInt(face.getGlyph().getMetrics().getHeight());
break;
}
// determine cap height
if (data.capHeight == 1) throw new GdxRuntimeException("No cap character found in font");
data.ascent = data.ascent - data.capHeight;
data.down = -data.lineHeight;
if (flip) {
data.ascent = -data.ascent;
data.down = -data.down;
}
// generate the glyphs
int maxGlyphHeight = (int)Math.ceil(data.lineHeight);
int pageWidth = MathUtils.nextPowerOfTwo((int)Math.sqrt(maxGlyphHeight * maxGlyphHeight * characters.length()));
boolean ownsAtlas = false;
if (atlas==null) {
ownsAtlas = true;
atlas = new PixmapPacker(pageWidth, pageWidth, Format.RGBA8888, 2, false);
}
packPrefix = packPrefix!=null ? packPrefix : "";
for (int i = 0; i < characters.length(); i++) {
char c = characters.charAt(i);
if (!FreeType.loadChar(face, c, FreeType.FT_LOAD_DEFAULT)) {
Gdx.app.log("FreeTypeFontGenerator", "Couldn't load char '" + c + "'");
continue;
}
if (!FreeType.renderGlyph(face.getGlyph(), FreeType.FT_RENDER_MODE_NORMAL)) {
Gdx.app.log("FreeTypeFontGenerator", "Couldn't render char '" + c + "'");
continue;
}
GlyphSlot slot = face.getGlyph();
GlyphMetrics metrics = slot.getMetrics();
Bitmap bitmap = slot.getBitmap();
Pixmap pixmap = bitmap.getPixmap(Format.RGBA8888);
Rectangle rect = atlas.pack(packPrefix + c, pixmap);
count++;
Glyph glyph = new Glyph();
glyph.width = pixmap.getWidth();
glyph.height = pixmap.getHeight();
glyph.xoffset = slot.getBitmapLeft();
glyph.yoffset = flip ? -slot.getBitmapTop() + (int)baseLine : -(glyph.height - slot.getBitmapTop()) - (int)baseLine;
glyph.xadvance = FreeType.toInt(metrics.getHoriAdvance());
glyph.srcX = (int)rect.x;
glyph.srcY = (int)rect.y;
data.setGlyph(c, glyph);
pixmap.dispose();
}
// generate kerning
for (int i = 0; i < characters.length(); i++) {
for (int j = 0; j < characters.length(); j++) {
char firstChar = characters.charAt(i);
Glyph first = data.getGlyph(firstChar);
if (first == null) continue;
char secondChar = characters.charAt(j);
Glyph second = data.getGlyph(secondChar);
if (second == null) continue;
int kerning = FreeType.getKerning(face, FreeType.getCharIndex(face, firstChar),
FreeType.getCharIndex(face, secondChar), 0);
if (kerning == 0) continue;
first.setKerning(secondChar, FreeType.toInt(kerning));
}
}
if (ownsAtlas) {
TextureAtlas textureAtlas = atlas.generateTextureAtlas(TextureFilter.Nearest, TextureFilter.Nearest, false);
data.region = new TextureRegion(textureAtlas.getRegions().get(0).getTexture());
}
return data;
}
/** Cleans up all resources of the generator. Call this if you no longer use the generator. */
@Override
public void dispose () {
FreeType.doneFace(face);
FreeType.doneFreeType(library);
}
/** {@link BitmapFontData} used for fonts generated via the {@link FreeTypeFontGenerator}. The texture storing the glyphs is
* held in memory, thus the {@link #getImagePath()} and {@link #getFontFile()} methods will return null.
*
* @author mzechner */
public static class FreeTypeBitmapFontData extends BitmapFontData {
public TextureRegion region;
public TextureRegion getTextureRegion () {
return region;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment