/** * Copyright (c) 2012, Matt DesLauriers All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * * Redistributions in binary * form must reproduce the above copyright notice, this list of conditions and * the following disclaimer in the documentation and/or other materials provided * with the distribution. * * * Neither the name of the Matt DesLauriers nor the names * of his contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package mdesl.graphics.text; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.StringTokenizer; import mdesl.graphics.SpriteBatch; import mdesl.graphics.Texture; import mdesl.graphics.TextureRegion; /** * A simple font implementation using text files from BMFont. A * more robust solution might use distance field rendering for smooth * size-independent scaling. * * @author davedes */ public class BitmapFont { //TODO: fix up baseLine, ascent, descent, etc. int lineHeight; int baseLine; int descent; int pages; Glyph[] glyphs; TextureRegion[] texturePages; class Glyph { int chr; int x, y, width, height; int xoffset, yoffset, xadvance; int[] kerning; int page; TextureRegion region; /** * Get the kerning offset between this character and the specified character. * @param c The other code point * @return the kerning offset */ public int getKerning(int c) { if (kerning==null) return 0; return kerning[c]; } void updateRegion(TextureRegion tex) { if (region==null) region = new TextureRegion(tex, 0, 0, tex.getWidth(), tex.getHeight()); region.set(tex, x, y, width, height); } } public BitmapFont(URL fontDef, URL texture) throws IOException { this(fontDef, new Texture(texture)); } public BitmapFont(URL fontDef, Texture texture) throws IOException { this(fontDef.openStream(), new TextureRegion(texture)); } public BitmapFont(URL fontDef, TextureRegion texture) throws IOException { this(fontDef.openStream(), texture); } public BitmapFont(InputStream fontDef, TextureRegion texture) throws IOException { this(fontDef, new TextureRegion[] { texture }); } public BitmapFont(InputStream fontDef, TextureRegion[] texturePages) throws IOException { this(fontDef, Charset.defaultCharset(), texturePages); } public BitmapFont(InputStream fontDef, Charset charset, TextureRegion[] texturePages) throws IOException { this.texturePages = texturePages; parseFont(fontDef, charset); } public int getLineHeight() { return lineHeight; } public TextureRegion[] getTexturePages() { return texturePages; } public void drawText(SpriteBatch batch, CharSequence text, int x, int y) { drawText(batch, text, x, y, 0, text.length()); } public void drawText(SpriteBatch batch, CharSequence text, int x, int y, int start, int end) { Glyph lastGlyph = null; for (int i=start; i<end; i++) { char c = text.charAt(i); //TODO: make unsupported glyphs a bit cleaner... if (c > glyphs.length || c < 0) continue; Glyph g = glyphs[c]; if (g==null) continue; if (lastGlyph!=null) x += lastGlyph.getKerning(c); lastGlyph = g; batch.draw(g.region, x + g.xoffset, y + g.yoffset, g.width, g.height); x += g.xadvance; } } public int getBaseline() { return baseLine; } public int getWidth(CharSequence text) { return getWidth(text, 0, text.length()); } public int getWidth(CharSequence text, int start, int end) { Glyph lastGlyph = null; int width = 0; for (int i=start; i<end; i++) { char c = text.charAt(i); //TODO: make unsupported glyphs a bit cleaner... if (c > glyphs.length || c < 0) continue; Glyph g = glyphs[c]; if (g==null) continue; if (lastGlyph!=null) width += lastGlyph.getKerning(c); lastGlyph = g; // width += g.width + g.xoffset; // width += g.width + g.xoffset; width += g.xadvance; } return width; } private static String parse(String line, String tag) { tag += "="; int start = line.indexOf(tag); if (start==-1) return ""; int end = line.indexOf(' ', start+tag.length()); if (end==-1) end = line.length(); return line.substring(start+tag.length(), end); } private static int parseInt(String line, String tag) throws IOException { try { return Integer.parseInt(parse(line, tag)); } catch (NumberFormatException e) { throw new IOException("data for "+tag+" is corrupt/missing: "+parse(line, tag)); } } protected void parseFont(InputStream fontFile, Charset charset) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(fontFile, charset), 512); String info = br.readLine(); String common = br.readLine(); lineHeight = parseInt(common, "lineHeight"); baseLine = parseInt(common, "base"); pages = parseInt(common, "pages"); //ignore file name, let user specify texture ... String line = ""; ArrayList<Glyph> glyphsList = null; int maxCodePoint = 0; while (true) { line = br.readLine(); if (line == null) break; if (line.startsWith("chars")) { // System.out.println(line); int count = parseInt(line, "count"); glyphsList = new ArrayList<Glyph>(count); continue; } if (line.startsWith("kernings ")) break; if (!line.startsWith("char ")) continue; Glyph glyph = new Glyph(); StringTokenizer tokens = new StringTokenizer(line, " ="); tokens.nextToken(); tokens.nextToken(); int ch = Integer.parseInt(tokens.nextToken()); if (ch > Character.MAX_VALUE) continue; if (glyphsList==null) //incase some doofus deleted a line in the font def glyphsList = new ArrayList<Glyph>(); glyphsList.add(glyph); glyph.chr = ch; if (ch > maxCodePoint) maxCodePoint = ch; tokens.nextToken(); glyph.x = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); glyph.y = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); glyph.width = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); glyph.height = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); glyph.xoffset = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); glyph.yoffset = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); glyph.xadvance = Integer.parseInt(tokens.nextToken()); //page ID tokens.nextToken(); glyph.page = Integer.parseInt(tokens.nextToken()); if (glyph.page > texturePages.length) throw new IOException("not enough texturePages supplied; glyph "+glyph.chr+" expects page index "+glyph.page); glyph.updateRegion(texturePages[glyph.page]); if (glyph.width > 0 && glyph.height > 0) descent = Math.min(baseLine + glyph.yoffset, descent); } glyphs = new Glyph[maxCodePoint + 1]; for (Glyph g : glyphsList) glyphs[g.chr] = g; int kernCount = parseInt(line, "count"); while (true) { line = br.readLine(); if (line == null) break; if (!line.startsWith("kerning ")) break; StringTokenizer tokens = new StringTokenizer(line, " ="); tokens.nextToken(); tokens.nextToken(); int first = Integer.parseInt(tokens.nextToken()); tokens.nextToken(); int second = Integer.parseInt(tokens.nextToken()); if (first < 0 || first > Character.MAX_VALUE || second < 0 || second > Character.MAX_VALUE) continue; Glyph glyph = glyphs[first]; tokens.nextToken(); int offset = Integer.parseInt(tokens.nextToken()); if (glyph.kerning==null) { glyph.kerning = new int[maxCodePoint + 1]; } glyph.kerning[second] = offset; } try { fontFile.close(); br.close(); } catch (IOException e) { //silent } } /** * Disposes all texture pages associated with this font. */ public void dispose() { for (TextureRegion t : getTexturePages()) t.getTexture().dispose(); } }