/**
* Copyright 2012 Jason Sorensen (sorensenj@smert.net)
*
* 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.
*/
package net.smert.frameworkgl.opengl.font;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Iterator;
import java.util.StringTokenizer;
import net.smert.frameworkgl.Files;
import net.smert.frameworkgl.Fw;
import net.smert.frameworkgl.opengl.Texture;
import net.smert.frameworkgl.opengl.renderable.AbstractRenderable;
import net.smert.frameworkgl.opengl.renderable.Renderable;
import net.smert.frameworkgl.utils.HashMapIntGeneric;
import net.smert.frameworkgl.utils.HashMapIntInt;
import net.smert.frameworkgl.utils.HashMapIntString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* http://www.angelcode.com/products/bmfont/doc/file_format.html
*
* @author Jason Sorensen <sorensenj@smert.net>
*/
public class AngelCodeFont implements GLFont {
private final static Logger log = LoggerFactory.getLogger(AngelCodeFont.class);
private boolean fontBold; // The font is bold
private boolean fontItalic; // The font is italic
// Set to 1 if the monochrome characters have been packed into each of the texture
// channels. In this case alphaChnl describes what is stored in each channel.
private boolean fontPacked;
private boolean fontSmooth; // Set to 1 if smoothing was turned on
private boolean fontUnicode; // Set to 1 if it is the unicode charset
// Set to 0 if the channel holds the glyph data, 1 if it holds the outline, 2 if it
// holds the glyph and the outline, 3 if its set to zero, and 4 if its set to one.
private int fontAlphaChannel;
private int fontAntiAliasing; // The supersampling level used. 1 means no supersampling was used.
private int fontBase; // The number of pixels from the absolute top of the line to the base of the characters
// Set to 0 if the channel holds the glyph data, 1 if it holds the outline, 2 if it
// holds the glyph and the outline, 3 if its set to zero, and 4 if its set to one.
private int fontBlueChannel;
// Set to 0 if the channel holds the glyph data, 1 if it holds the outline, 2 if it
// holds the glyph and the outline, 3 if its set to zero, and 4 if its set to one.
private int fontGreenChannel;
private int fontLineHeight; // This is the distance in pixels between each line of text
private int fontOutline; // The outline thickness for the characters
private int fontPaddingDown; // The padding for each character (up, right, down, left)
private int fontPaddingLeft;
private int fontPaddingRight;
private int fontPaddingUp;
private int fontPages; // The number of texture pages included in the font
// Set to 0 if the channel holds the glyph data, 1 if it holds the outline, 2 if it
// holds the glyph and the outline, 3 if its set to zero, and 4 if its set to one.
private int fontRedChannel;
private int fontScaleHeight; // The height of the texture, normally used to scale the y pos of the character image
private int fontScaleWidth; // The width of the texture, normally used to scale the x pos of the character image
private int fontSize; // The size of the true type font
private int fontSpacingHorizontal; // The spacing for each character (horizontal, vertical)
private int fontSpacingVertical;
private int fontStretchHeight; // The font height stretch in percentage. 100% means no stretch.
private int totalChars;
private int totalKernings;
private Glyph missingGlyph;
private final HashMapIntString pages; // The page id. The texture file name.
private final HashMapIntGeneric<Character> characters;
private final HashMapIntGeneric<Glyph> glyphs;
private String fontCharset; // The name of the OEM charset used (when not unicode)
private String fontFace; // This is the name of the true type font
public AngelCodeFont() {
pages = new HashMapIntString();
characters = new HashMapIntGeneric<>();
glyphs = new HashMapIntGeneric<>();
}
private void addChar(StringTokenizer tokenizer) {
Character character = new Character();
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
String[] keyValue = token.split("=");
if (keyValue.length != 2) {
log.warn("Skipping invalid key value for char line: {}", token);
continue;
}
// Parse key and value
String key = keyValue[0];
String value = keyValue[1];
switch (key) {
case "chnl":
character.setChannel(Integer.parseInt(value));
break;
case "height":
character.setHeight(Integer.parseInt(value));
break;
case "id":
character.setId(Integer.parseInt(value));
break;
case "page":
character.setPage(Integer.parseInt(value));
break;
case "width":
character.setWidth(Integer.parseInt(value));
break;
case "x":
character.setX(Integer.parseInt(value));
break;
case "xadvance":
character.setXAdvance(Integer.parseInt(value));
break;
case "xoffset":
character.setXOffset(Integer.parseInt(value));
break;
case "y":
character.setY(Integer.parseInt(value));
break;
case "yoffset":
character.setYOffset(Integer.parseInt(value));
break;
default:
log.warn("Skipping unknown key value for char line: {}", token);
}
}
characters.put(character.getId(), character);
}
private void addChars(StringTokenizer tokenizer) {
String countToken = tokenizer.nextToken();
String[] countKeyValue = countToken.split("=");
if (countKeyValue.length != 2) {
log.warn("Skipping invalid key value for count token during chars line: {}", countToken);
return;
}
if (!countKeyValue[0].equals("count")) {
log.warn("Skipping invalid key for count token during chars line: {}", countToken);
}
totalChars = Integer.parseInt(countKeyValue[1]);
}
private void addCommon(StringTokenizer tokenizer) {
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
String[] keyValue = token.split("=");
if (keyValue.length != 2) {
log.warn("Skipping invalid key value for common line: {}", token);
continue;
}
// Parse key and value
String key = keyValue[0];
String value = keyValue[1];
switch (key) {
case "alphaChnl":
fontAlphaChannel = Integer.parseInt(value);
break;
case "base":
fontBase = Integer.parseInt(value);
break;
case "blueChnl":
fontBlueChannel = Integer.parseInt(value);
break;
case "greenChnl":
fontGreenChannel = Integer.parseInt(value);
break;
case "lineHeight":
fontLineHeight = Integer.parseInt(value);
break;
case "packed":
fontPacked = (Integer.parseInt(value) == 1);
break;
case "pages":
fontPages = Integer.parseInt(value);
break;
case "redChnl":
fontRedChannel = Integer.parseInt(value);
break;
case "scaleH":
fontScaleHeight = Integer.parseInt(value);
break;
case "scaleW":
fontScaleWidth = Integer.parseInt(value);
break;
default:
log.warn("Skipping unknown key value for common line: {}", token);
}
}
}
private void addInfo(StringTokenizer tokenizer) {
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
String[] keyValue = token.split("=");
if (keyValue.length != 2) {
log.warn("Skipping invalid key value for info line: {}", token);
continue;
}
// Parse key and value
String key = keyValue[0];
String value = keyValue[1];
switch (key) {
case "aa":
fontAntiAliasing = Integer.parseInt(value);
break;
case "bold":
fontBold = (Integer.parseInt(value) == 1);
break;
case "charset":
fontCharset = value;
break;
case "face":
fontFace = value;
break;
case "italic":
fontItalic = (Integer.parseInt(value) == 1);
break;
case "outline":
fontOutline = Integer.parseInt(value);
break;
case "padding":
String[] padding = value.split(",");
if (padding.length != 4) {
log.warn("Invalid padding defined: {}", token);
continue;
}
fontPaddingUp = Integer.parseInt(padding[0]);
fontPaddingRight = Integer.parseInt(padding[1]);
fontPaddingDown = Integer.parseInt(padding[2]);
fontPaddingLeft = Integer.parseInt(padding[3]);
break;
case "size":
fontSize = Integer.parseInt(value);
break;
case "smooth":
fontSmooth = (Integer.parseInt(value) == 1);
break;
case "spacing":
String[] spacing = value.split(",");
if (spacing.length != 2) {
log.warn("Invalid spacing defined: {}", token);
continue;
}
fontSpacingHorizontal = Integer.parseInt(spacing[0]);
fontSpacingVertical = Integer.parseInt(spacing[1]);
break;
case "stretchH":
fontStretchHeight = Integer.parseInt(value);
break;
case "unicode":
fontUnicode = (Integer.parseInt(value) == 1);
break;
default:
log.warn("Skipping unknown key value for info line: {}", token);
}
}
}
private void addKerning(StringTokenizer tokenizer) {
String firstToken = tokenizer.nextToken();
String[] firstKeyValue = firstToken.split("=");
if (firstKeyValue.length != 2) {
throw new RuntimeException("Invalid key value for first token during kerning line: " + firstToken);
}
if (!firstKeyValue[0].equals("first")) {
throw new RuntimeException("Invalid key for first token during kerning line: " + firstToken);
}
String secondToken = tokenizer.nextToken();
String[] secondKeyValue = secondToken.split("=");
if (secondKeyValue.length != 2) {
throw new RuntimeException("Invalid key value for second token during kerning line: " + secondToken);
}
if (!secondKeyValue[0].equals("second")) {
throw new RuntimeException("Invalid key for second token during kerning line: " + firstToken);
}
String amountToken = tokenizer.nextToken();
String[] amountKeyValue = amountToken.split("=");
if (amountKeyValue.length != 2) {
throw new RuntimeException("Invalid key value for amount token during kerning line: " + amountToken);
}
if (!amountKeyValue[0].equals("amount")) {
throw new RuntimeException("Invalid key for amount token during kerning line: " + firstToken);
}
int first = Integer.parseInt(firstKeyValue[1]);
int second = Integer.parseInt(secondKeyValue[1]);
int amount = Integer.parseInt(amountKeyValue[1]);
Character character = characters.get(first);
if (character == null) {
character = new Character();
character.setId(first);
characters.put(first, character);
}
character.setKerning(second, amount);
}
private void addKernings(StringTokenizer tokenizer) {
String countToken = tokenizer.nextToken();
String[] countKeyValue = countToken.split("=");
if (countKeyValue.length != 2) {
log.warn("Skipping invalid key value for count token during kernings line: {}", countToken);
return;
}
if (!countKeyValue[0].equals("count")) {
log.warn("Skipping invalid key for count token during kernings line: {}", countToken);
}
totalKernings = Integer.parseInt(countKeyValue[1]);
}
private void addPage(StringTokenizer tokenizer) {
String idToken = tokenizer.nextToken();
String[] idKeyValue = idToken.split("=");
if (idKeyValue.length != 2) {
throw new RuntimeException("Invalid key value for ID token during page line: " + idToken);
}
if (!idKeyValue[0].equals("id")) {
throw new RuntimeException("Invalid key for ID token during page line: " + idToken);
}
String fileToken = tokenizer.nextToken();
String[] fileKeyValue = fileToken.split("=");
if (fileKeyValue.length != 2) {
throw new RuntimeException("Invalid key value for file token during page line: " + fileToken);
}
if (!fileKeyValue[0].equals("file")) {
throw new RuntimeException("Invalid key for file token during page line: " + idToken);
}
int id = Integer.parseInt(idKeyValue[1]);
String filename = fileKeyValue[1];
// Remove leading double quotes
while (filename.startsWith("\"")) {
filename = filename.substring(1);
}
// Remove trailing double quotes
while (filename.endsWith("\"")) {
filename = filename.substring(0, filename.length() - 1);
}
pages.put(id, filename);
}
private void parse(String line) {
StringTokenizer tokenizer = new StringTokenizer(line);
if (!tokenizer.hasMoreTokens()) {
return;
}
int totalTokens = tokenizer.countTokens();
String token = tokenizer.nextToken();
switch (token) {
case "char":
if (totalTokens >= 7) {
addChar(tokenizer);
} else {
log.warn("Invalid char definition: {}", line);
}
break;
case "chars":
if (totalTokens == 2) {
addChars(tokenizer);
} else {
log.warn("Invalid chars definition: {}", line);
}
break;
case "common":
if (totalTokens > 2) {
addCommon(tokenizer);
} else {
log.warn("Invalid common definition: {}", line);
}
break;
case "info":
if (totalTokens > 2) {
addInfo(tokenizer);
} else {
log.warn("Invalid info definition: {}", line);
}
break;
case "kerning":
if (totalTokens == 4) {
addKerning(tokenizer);
} else {
log.warn("Invalid kerning definition: {}", line);
}
break;
case "kernings":
if (totalTokens == 2) {
addKernings(tokenizer);
} else {
log.warn("Invalid kernings definition: {}", line);
}
break;
case "page":
if (totalTokens == 3) {
addPage(tokenizer);
} else {
log.warn("Invalid page definition: {}", line);
}
break;
default:
log.warn("Skipped line with an unsupported token: {}", line);
}
}
private void read(String filename) throws IOException {
Files.FileAsset fileAsset = Fw.files.getFont(filename);
try (InputStream is = fileAsset.openStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader reader = new BufferedReader(isr)) {
String line;
while ((line = reader.readLine()) != null) {
parse(line);
}
}
}
private void reset() {
fontBold = false;
fontItalic = false;
fontPacked = false;
fontSmooth = false;
fontUnicode = false;
fontAlphaChannel = -1;
fontAntiAliasing = 0;
fontBase = 0;
fontBlueChannel = -1;
fontGreenChannel = -1;
fontLineHeight = 0;
fontOutline = 0;
fontPaddingDown = 0;
fontPaddingLeft = 0;
fontPaddingRight = 0;
fontPaddingUp = 0;
fontPages = 0;
fontRedChannel = -1;
fontScaleHeight = 0;
fontScaleWidth = 0;
fontSize = 0;
fontSpacingHorizontal = 0;
fontSpacingVertical = 0;
fontStretchHeight = 0;
totalChars = 0;
totalKernings = 0;
pages.clear();
characters.clear();
glyphs.clear();
fontCharset = "";
fontFace = "";
}
public void destroy() {
characters.clear();
Iterator<Glyph> iteratorGlyph = glyphs.values().iterator();
while (iteratorGlyph.hasNext()) {
Glyph glyph = iteratorGlyph.next();
glyph.renderable.destroy();
}
glyphs.clear();
Iterator<HashMapIntString.Entry> iteratorPage = pages.entrySet().iterator();
while (iteratorPage.hasNext()) {
HashMapIntString.Entry entry = iteratorPage.next();
String filename = entry.getValue();
Texture texture = Renderable.texturePool.remove(filename);
if (texture != null) {
texture.destroy();
}
}
pages.clear();
}
public int getFontAlphaChannel() {
return fontAlphaChannel;
}
public int getFontAntiAliasing() {
return fontAntiAliasing;
}
public int getFontBase() {
return fontBase;
}
public int getFontBlueChannel() {
return fontBlueChannel;
}
public int getFontGreenChannel() {
return fontGreenChannel;
}
public int getFontLineHeight() {
return fontLineHeight;
}
public int getFontOutline() {
return fontOutline;
}
public int getFontPaddingDown() {
return fontPaddingDown;
}
public int getFontPaddingLeft() {
return fontPaddingLeft;
}
public int getFontPaddingRight() {
return fontPaddingRight;
}
public int getFontPaddingUp() {
return fontPaddingUp;
}
public int getFontPages() {
return fontPages;
}
public int getFontRedChannel() {
return fontRedChannel;
}
public int getFontScaleHeight() {
return fontScaleHeight;
}
public int getFontScaleWidth() {
return fontScaleWidth;
}
public int getFontSize() {
return fontSize;
}
public int getFontSpacingHorizontal() {
return fontSpacingHorizontal;
}
public int getFontSpacingVertical() {
return fontSpacingVertical;
}
public int getFontStretchHeight() {
return fontStretchHeight;
}
public int getTotalChars() {
return totalChars;
}
public int getTotalKernings() {
return totalKernings;
}
public Glyph getGlyph(int codePoint) {
Glyph glyph = glyphs.get(codePoint);
if (glyph == null) {
Character character = characters.get(codePoint);
if (character == null) {
return missingGlyph;
}
glyph = new Glyph();
glyph.character = character;
glyphs.put(codePoint, glyph);
}
return glyph;
}
public Glyph getMissingGlyph() {
return missingGlyph;
}
public void setMissingGlyph(Glyph missingGlyph) {
this.missingGlyph = missingGlyph;
}
public HashMapIntString getPages() {
return pages;
}
public HashMapIntGeneric<Character> getCharacters() {
return characters;
}
public String getFontCharset() {
return fontCharset;
}
public String getFontFace() {
return fontFace;
}
public String getPage(int page) {
return pages.get(page);
}
public boolean isFontBold() {
return fontBold;
}
public boolean isFontItalic() {
return fontItalic;
}
public boolean isFontPacked() {
return fontPacked;
}
public boolean isFontSmooth() {
return fontSmooth;
}
public boolean isFontUnicode() {
return fontUnicode;
}
public void load(String filename) throws IOException {
log.info("Loading Angel Code Font: {}", filename);
reset();
read(filename);
}
@Override
public int getCharacterAdvance(char currentCharacter, char nextCharacter, float sizeX) {
int currentCodePoint = (int) currentCharacter;
int nextCodePoint = (int) nextCharacter;
Character character = characters.get(currentCodePoint);
if (character == null) {
return -1;
}
return (int) ((character.getXAdvance() + character.getKerning(nextCodePoint)) * sizeX);
}
@Override
public int getWidth(String text, float sizeX) {
int length = 0;
for (int i = 0; i < text.length(); i++) {
char currentCharacter = text.charAt(i);
char nextCharacter = 0;
if (i < text.length() - 1) {
nextCharacter = text.charAt(i + 1);
}
int advance = getCharacterAdvance(currentCharacter, nextCharacter, sizeX);
if (advance != -1) {
length += advance;
}
}
return length;
}
@Override
public String toString() {
return "(fontBold= " + fontBold + " fontItalic= " + fontItalic + " fontPacked= " + fontPacked
+ " fontSmooth= " + fontSmooth + " fontUnicode= " + fontUnicode
+ " fontAlphaChannel= " + fontAlphaChannel + " fontAntiAliasing= " + fontAntiAliasing
+ " fontBase= " + fontBase + " fontBlueChannel= " + fontBlueChannel
+ " fontGreenChannel= " + fontGreenChannel + " fontLineHeight= " + fontLineHeight
+ " fontOutline= " + fontOutline + " fontPaddingDown= " + fontPaddingDown
+ " fontPaddingLeft= " + fontPaddingLeft + " fontPaddingRight= " + fontPaddingRight
+ " fontPaddingUp= " + fontPaddingUp + " fontPages= " + fontPages + " fontRedChannel= " + fontRedChannel
+ " fontScaleHeight= " + fontScaleHeight + " fontScaleWidth= " + fontScaleWidth
+ " fontSize= " + fontSize + " fontSpacingHorizontal= " + fontSpacingHorizontal
+ " fontSpacingVertical= " + fontSpacingVertical + " fontStretchHeight= " + fontStretchHeight
+ " totalChars= " + totalChars + " totalKernings= " + totalKernings
+ " pages(size)= " + pages.size() + " characters(size)= " + characters.size()
+ " fontCharset= " + fontCharset + " fontFace= " + fontFace + ")";
}
public static class Character {
// The texture channel where the character image is found (1 = blue,
// 2 = green, 4 = red, 8 = alpha, 15 = all channels)
private int channel;
private int height; // The height of the character image in the texture
private int id; // The character id
private int page; // The texture page where the character image is found
private int width; // The width of the character image in the texture
private int x; // The left position of the character image in the texture
private int xAdvance; // How much the current position should be advanced after drawing the character
// How much the current position should be offset when copying the image from the texture to the screen
private int xOffset;
private int y; // The top position of the character image in the texture
// How much the current position should be offset when copying the image from the texture to the screen
private int yOffset;
// The second character id
// How much the x position should be adjusted when drawing the second character immediately following the first
private final HashMapIntInt kerning;
public Character() {
kerning = new HashMapIntInt();
}
public int getChannel() {
return channel;
}
public void setChannel(int channel) {
this.channel = channel;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getKerning(int second) {
int amount = kerning.get(second);
if (amount == HashMapIntInt.NOT_FOUND) {
return 0;
}
return amount;
}
public void setKerning(int second, int amount) {
kerning.put(second, amount);
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getXAdvance() {
return xAdvance;
}
public void setXAdvance(int xAdvance) {
this.xAdvance = xAdvance;
}
public int getXOffset() {
return xOffset;
}
public void setXOffset(int xOffset) {
this.xOffset = xOffset;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getYOffset() {
return yOffset;
}
public void setYOffset(int yOffset) {
this.yOffset = yOffset;
}
@Override
public String toString() {
return "(id= " + id + " channel= " + channel + " height= " + height + " page= " + page + " width= " + width
+ " x= " + x + " xAdvance= " + xAdvance + " xOffset= " + xOffset + " y= " + y
+ " yOffset= " + yOffset + " kerning(size)= " + kerning.size();
}
}
public static class Glyph {
public AbstractRenderable renderable;
public Character character;
}
}