Hi,
I had problems finding appropriate fnt fonts so I’ve implemented a loader for much more popular ttf
Hope it will be useful
Here’s the loader:
package com.jme3.font.plugins;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.font.GlyphMetrics;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLoader;
import com.jme3.asset.FontKey;
import com.jme3.font.BitmapCharacter;
import com.jme3.font.BitmapCharacterSet;
import com.jme3.font.BitmapFont;
import com.jme3.material.Material;
import com.jme3.material.MaterialDef;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.plugins.AWTLoader;
/**
- This class loads the font stored in TrueType file format.
-
@author Marcin Roguski
*/
public class TTFFontLoader implements AssetLoader {
/**
-
@see AssetLoader
-
@throws IllegalArgumentException
-
an exception is thrown if the asset key is not of FontKey type<br />
*/
@Override
public Object load(AssetInfo assetInfo) throws IOException {
if(assetInfo.getKey() instanceof FontKey) {
Graphics2D graphics = this.getGraphics();
FontKey fontKey = (FontKey)assetInfo.getKey();
Font font = this.getFont(assetInfo);
graphics.setFont(font);
FontData fontData = this.calculateFontData(graphics, fontKey);
Texture t = this.prepareTexture(fontData, graphics, fontKey.getColor());
this.setPagesForFont(assetInfo, fontData.bitmapFont, t);
return fontData.bitmapFont;
} else {
throw new IllegalArgumentException("The given asset key should be of type: " + FontKey.class.getName());
}
}
/**
- Creates the AWT graphics object to render the fonts.
-
@return AWT graphics object
*/
protected Graphics2D getGraphics() {
GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
return gc.createCompatibleImage(1, 1, Transparency.TRANSLUCENT).createGraphics();
}
/**
- This method loads AWT type font from the stream indicated by assetInfo.
-
@param assetInfo
-
the information about font asset<br />
-
@return AWT type font
-
@throws IOException
-
an exception is thrown if there are problems with the stream<br />
-
itself or with the font type (it should be TrueType)<br />
*/
protected Font getFont(AssetInfo assetInfo) throws IOException {
try {
FontKey fontKey = (FontKey)assetInfo.getKey();
return Font.createFont(fontKey.getType(), assetInfo.openStream()).deriveFont(fontKey.getStyle(), fontKey.getSize());
} catch(FontFormatException e) {
throw new IOException(e.getMessage(), e);
}
}
/**
- This method reads all available fonts and prepares the basic data about
- its characters.
-
@param graphics
-
the graphics object that will later prepare the image of the fonts<br />
-
@param fontKey
-
the key containing essential font information<br />
-
@return the basic data about the font
*/
protected FontData calculateFontData(Graphics2D graphics, FontKey fontKey) {
Font font = graphics.getFont();
FontMetrics fontMetrics = graphics.getFontMetrics();
int glyphsAmount = font.getNumGlyphs(), i = 0;
int missingGlyphCode = font.getMissingGlyphCode();
char[] chars = new char[] {0};
FontData result = new FontData();
result.bitmapFont = new BitmapFont();
result.bitmapFont.setCharSet(new BitmapCharacterSet());
result.bitmapFont.getCharSet().setRenderedSize(font.getSize());
result.charList = new ArrayList<TTFFontLoader.CharacterData>();
while(i < glyphsAmount) {
GlyphVector gv = font.createGlyphVector(graphics.getFontRenderContext(), chars);
int glyphCode = gv.getGlyphCode(0);
GlyphMetrics gm = gv.getGlyphMetrics(0);
if(glyphCode != missingGlyphCode && glyphCode < glyphsAmount && result.bitmapFont.getCharSet().getCharacter((int)chars[0]) == null) {
BitmapCharacter bitmapCharacter = new BitmapCharacter();
bitmapCharacter.setWidth(Character.isWhitespace(chars[0]) ? (int)Math.ceil(gm.getAdvanceX()) : (int)Math.ceil(gm.getBounds2D().getWidth()));
bitmapCharacter.setXAdvance((int)Math.ceil(gm.getAdvanceX()));
result.bitmapFont.getCharSet().addCharacter((int)chars[0], bitmapCharacter);
result.imageWidth += bitmapCharacter.getWidth() + (int)Math.ceil(Math.abs(gm.getLSB()));//LSB only affects the render image width
result.imageHeight = (int)Math.ceil(Math.max(result.imageHeight, gv.getGlyphVisualBounds(0).getBounds2D().getHeight()));
result.charList.add(new CharacterData(chars[0], gm.getLSB(), bitmapCharacter));
++i;
} else if(Character.isWhitespace(chars[0])) {
++i;
}
chars[0] = (char)(chars[0] + 1);
}
int ascentBuffer = this.parseBuffer(fontKey.getAboveAscentBuffer(), result.imageHeight);
int descentBuffer = this.parseBuffer(fontKey.getAboveAscentBuffer(), result.imageHeight);
result.imageHeight += ascentBuffer + descentBuffer;
result.bitmapFont.getCharSet().setBase(fontMetrics.getMaxAscent()+ascentBuffer);//we move the base down by the value of ascentBuffer
result.bitmapFont.getCharSet().setLineHeight(fontMetrics.getMaxAscent() + fontMetrics.getMaxDescent()+ascentBuffer+descentBuffer);
result.bitmapFont.getCharSet().setRenderedSize(font.getSize());
return result;
}
/**
- This mehtod parsers the buffer value.
- The value should be a numeric value that ends in ‘%’, ‘px’ or nothing.
- ‘px’ is a default measure unit.
-
@param buffer the buffer value to be parsed
-
@param imageHeight the height of the image; required if the buffer value
- is given in percents
-
@return an integer value of buffer height in pixels
*/
protected int parseBuffer(String buffer, int imageHeight) {
if(buffer==null || buffer.isEmpty()) {
return 0;
}
if(buffer.endsWith("%")) {
String value = buffer.split("%")[0];
float percent = Float.parseFloat(value);
if(percent!=0.0f) {
percent = 1.0f/percent;
}
return (int)Math.round(imageHeight * percent);
}
else if(buffer.endsWith(“px”)) {
String value = buffer.split(“px”)[0];
return (int)Float.parseFloat(value);
}
return 0;
}
/**
- This method prepares the jme texture for the font. It also sets the rest
- of data required by font data.
-
@param fontData
-
the data of the font<br />
-
@param graphics
-
the object that renders the font<br />
-
@return font’s texture
*/
protected Texture prepareTexture(FontData fontData, Graphics2D graphics, Color fontColor) {
BufferedImage bufferedImage = graphics.getDeviceConfiguration().createCompatibleImage(fontData.imageWidth, fontData.imageHeight, Transparency.TRANSLUCENT);
//prepare the graphics object to render the font
Graphics2D imageGraphics = (Graphics2D)bufferedImage.getGraphics();
imageGraphics.setFont(graphics.getFont());
imageGraphics.setColor(fontColor);
imageGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//we need to mirror the image vertically (do not know why, but the engine redners it up-side down
)
AffineTransform at = new AffineTransform();
at.scale(1.0, -1.0);
at.translate(0.0, -bufferedImage.getHeight());
imageGraphics.transform(at);
//render each font and fill the x and y positions data and its height
int xPos = 0;
int base = fontData.bitmapFont.getCharSet().getBase();
char[] chars = new char[1];
for(CharacterData characterData : fontData.charList) {
chars[0] = characterData.character;
characterData.bitmapCharacter.setXOffset(characterData.lsb);
characterData.bitmapCharacter.setX(xPos);
characterData.bitmapCharacter.setY(0);
characterData.bitmapCharacter.setHeight(bufferedImage.getHeight());
//move the char so that when it is rendered it won’t hit other characters
xPos -= characterData.lsb;
imageGraphics.drawChars(chars, 0, 1, xPos, base);
xPos += characterData.lsb + characterData.bitmapCharacter.getWidth() + characterData.widthDelta;
}
//set the size of the bitmap
fontData.bitmapFont.getCharSet().setHeight(bufferedImage.getHeight());
fontData.bitmapFont.getCharSet().setWidth(bufferedImage.getWidth());
//create the texture
Image image = new AWTLoader().load(bufferedImage, false);
Texture texture = new Texture2D(image);
texture.setMagFilter(Texture.MagFilter.Bilinear);
texture.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
return texture;
}
/**
- This method sets the pages for font. For now only one page is supported.
-
@param assetInfo
-
the information about the font asset<br />
-
@param bitmapFont
-
the result bitmap font where the data will be stored<br />
-
@param textures
-
a list of textures to apply to the bitmap font pages (one texture<br />
-
per page)<br />
*/
@SuppressWarnings({“unchecked”, “rawtypes”})
protected void setPagesForFont(AssetInfo assetInfo, BitmapFont bitmapFont, Texture… textures) {
Material[] pages = new Material[textures.length];
MaterialDef spriteMat = (MaterialDef)assetInfo.getManager().loadAsset(new AssetKey(“Common/MatDefs/Gui/Gui.j3md”));
for(int i = 0; i < textures.length; ++i) {
Material mat = new Material(spriteMat);
mat.setTexture(“m_Texture”, textures);
mat.setColor(“m_Color”, ColorRGBA.White);
mat.setBoolean(“m_VertexColor”, true);
mat.getAdditionalRenderState().setBlendMode(BlendMode.AlphaAdditive);
pages = mat;
}
bitmapFont.setPages(pages);
}
/**
- Internal data aggregator class. It stores the result BitmapFont class. It
- also contains a list of characters data (calculated first and then passed
- to the method that renders the font texture). It also contains the size
- of the result image.
-
@author Marcin Roguski
*/
protected static class FontData {
public BitmapFont bitmapFont;
public List charList;
public int imageWidth;
public int imageHeight;
}
/**
- A class containing the data of a single character. The data contained:
- the character itself, the left (top) side bearing of the glyph (see
- java.awt.font.GlyphMetrics.getLSB()), the bitmap character that will be
- stored in the output data
-
@author Marcin Roguski
*/
protected static class CharacterData {
public char character;
public int lsb;
public int widthDelta;
public BitmapCharacter bitmapCharacter;
public CharacterData(char character, float lsb, BitmapCharacter bitmapCharacter) {
this.character = character;
this.lsb = (int)Math.floor(lsb);//that is the distace the character will be moved before rendering
this.widthDelta = (int)Math.ceil(Math.abs(lsb));//that is width modification caused by lsb
this.bitmapCharacter = bitmapCharacter;
}
}
}
And here is the font key class:
package com.jme3.asset;
import java.awt.Color;
import java.awt.Font;
import java.io.IOException;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.font.BitmapFont;
/** - A key to find fonts in asset manager.
-
@author Marcin Roguski
*/
public class FontKey extends AssetKey {
/**
- The type of the font. available values: Font.TRUETYPE_FONT,
- Font.TYPE1_FONT
/
protected int type;
/* The size of the font (should be greater than zero of course). /
protected int size;
/* The color of the font. */
protected Color color;
/**
- The style of the font; available values: Font.ITALIC, Font.PLAIN,
- Font.BOLD.
*/
protected int style;
/**
- The amount of pixels to be added above the ascent line. Available units:
- %, px.
*/
protected String aboveAscentBuffer;
/**
- The amount of pixels to be added below the descent line. Available units:
- %, px.
*/
protected String belowDescentBuffer;
/**
- Constructor with the aseet name. The constructor does not validate the
- given data. If the data is wrong it will arise exceptions during font
- creation. The aboveAscent and belowDescent buffers are both set to 0.
-
@param fontName
-
the name of the font asset<br />
-
@param type
-
the type of the font; available values: Font.TRUETYPE_FONT,<br />
-
Font.TYPE1_FONT<br />
-
@param color
-
the color of the font<br />
-
@param size
-
the size of the font (should be greater than zero of course)<br />
-
@param style
-
the style of the font; available values: Font.ITALIC, Font.PLAIN,<br />
-
Font.BOLD<br />
*/
public FontKey(String fontName, int type, Color color, int size, int style) {
super(fontName);
this.type = type;
this.color = color;
this.size = size;
this.style = style;
}
/**
- Constructor with the aseet name. The constructor does not validate the
- given data. If the data is wrong it will arise exceptions during font
- creation.
-
@param fontName
-
the name of the font asset<br />
-
@param type
-
the type of the font; available values: Font.TRUETYPE_FONT,<br />
-
Font.TYPE1_FONT<br />
-
@param color
-
the color of the font<br />
-
@param size
-
the size of the font (should be greater than zero of course)<br />
-
@param style
-
the style of the font; available values: Font.ITALIC, Font.PLAIN,<br />
-
Font.BOLD<br />
-
@param aboveAscentBuffer
-
the amount of pixels to be added above the ascent line<br />
-
@param belowDescentBuffer
-
the amount of pixels to be added below the descent line<br />
*/
public FontKey(String fontName, int type, Color color, int size, int style, String aboveAscentBuffer, String belowDescentBuffer) {
this(fontName, type, color, size, style);
this.aboveAscentBuffer = aboveAscentBuffer;
this.belowDescentBuffer = belowDescentBuffer;
}
/**
- This method returns the type of the font.
-
@return the type of the font
*/
public int getType() {
return type;
}
/**
- This method returns the color of the font.
-
@return the color of the font
*/
public Color getColor() {
return color;
}
/**
- This method returns the size of the font.
-
@return the size of the font
*/
public int getSize() {
return size;
}
/**
- This method returns the style of the font.
-
@return the style of the font
*/
public int getStyle() {
return style;
}
/**
- This method returns the amount of pixels to be added above the ascent
- line.
-
@return the amount of pixels to be added above the ascent line
*/
public String getAboveAscentBuffer() {
return aboveAscentBuffer;
}
/**
- This method returns the amount of pixels to be added below the descent
- line.
@return the amount of pixels to be added below the descent line
*/
public String getBelowDescentBuffer() {
return belowDescentBuffer;
}
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);
OutputCapsule oc = ex.getCapsule(this);
oc.write(type, “type”, Font.TRUETYPE_FONT);
oc.write(size, “size”, 18);
oc.write(style, “style”, Font.PLAIN);
oc.write(new float[] {color.getRed(), color.getGreen(), color.getBlue()}, “color”, null);
}
@Override
public void read(JmeImporter im) throws IOException {
super.read(im);
InputCapsule ic = im.getCapsule(this);
type = ic.readInt(“type”, Font.TRUETYPE_FONT);
size = ic.readInt(“size”, 18);
style = ic.readInt(“style”, Font.PLAIN);
float[] colorTable = ic.readFloatArray(“color”, new float[] {0.0f, 0.0f, 0.0f});
color = new Color(colorTable[0], colorTable[1], colorTable[2]);
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((color == null) ? 0 : color.hashCode());
result = prime * result + size;
result = prime * result + style;
result = prime * result + type;
return result;
}
@Override
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(!super.equals(obj)) {
return false;
}
if(getClass() != obj.getClass()) {
return false;
}
FontKey other = (FontKey)obj;
if(color == null) {
if(other.color != null) {
return false;
}
} else if(!color.equals(other.color)) {
return false;
}
if(size != other.size) {
return false;
}
if(style != other.style) {
return false;
}
if(type != other.type) {
return false;
}
return true;
}
}