View Javadoc
1   package org.newdawn.slick;
2   
3   import java.io.BufferedReader;
4   import java.io.IOException;
5   import java.io.InputStream;
6   import java.io.InputStreamReader;
7   import java.util.ArrayList;
8   import java.util.HashMap;
9   import java.util.LinkedHashMap;
10  import java.util.List;
11  import java.util.Map;
12  import java.util.Map.Entry;
13  import java.util.StringTokenizer;
14  
15  import org.newdawn.slick.opengl.renderer.Renderer;
16  import org.newdawn.slick.opengl.renderer.SGL;
17  import org.newdawn.slick.util.Log;
18  import org.newdawn.slick.util.ResourceLoader;
19  
20  import javax.annotation.Nonnull;
21  import javax.annotation.Nullable;
22  
23  /**
24   * A font implementation that will parse BMFont format font files. The font files can be output
25   * by Hiero, which is included with Slick, and also the AngelCode font tool available at:
26   * 
27   * <a
28   * href="http://www.angelcode.com/products/bmfont/">http://www.angelcode.com/products/bmfont/</a>
29   * 
30   * This implementation copes with both the font display and kerning information
31   * allowing nicer looking paragraphs of text. Note that this utility only
32   * supports the text BMFont format definition file.
33   * 
34   * @author kevin
35   * @author Nathan Sweet <misc@n4te.com>
36   */
37  public class AngelCodeFont implements Font {
38      /** The renderer to use for all GL operations */
39      private static final SGL GL = Renderer.get();
40  
41      /**
42       * The line cache size, this is how many lines we can render before starting
43       * to regenerate lists
44       */
45      private static final int DISPLAY_LIST_CACHE_SIZE = 200;
46  
47      /** The highest character that AngelCodeFont will support. */
48      private static final int MAX_CHAR = 255;
49  
50      /** True if this font should use display list caching */
51      private boolean displayListCaching = true;
52  
53      /** The image containing the bitmap font */
54      private Image fontImage;
55      /** The characters building up the font */
56      private Glyph[] chars;
57      /** The height of a line */
58      private int lineHeight;
59      /** The first display list ID */
60      private int baseDisplayListID = -1;
61      /** The eldest display list ID */
62      private int eldestDisplayListID;
63      /** The eldest display list  */
64      private DisplayList eldestDisplayList;
65  
66      private boolean singleCase = false;
67      private short ascent;
68      private short descent;
69      
70      /** The display list cache for rendered lines */
71      private final Map<CharSequence, DisplayList> displayLists = new LinkedHashMap<CharSequence, DisplayList>(DISPLAY_LIST_CACHE_SIZE, 1, true) {     
72          /**
73           * 
74           */
75          private static final long serialVersionUID = 1L;
76  
77          protected boolean removeEldestEntry(@Nonnull Entry<CharSequence, DisplayList> eldest) {
78              eldestDisplayList = eldest.getValue();
79              eldestDisplayListID = eldestDisplayList.id;
80  
81              return false;
82          }
83      };
84  
85  
86      /**
87       * Create a new font based on a font definition from AngelCode's tool and
88       * the font image generated from the tool.
89       *
90       * @param fntFile
91       *            The location of the font defnition file
92       * @param image
93       *            The image to use for the font
94       * @throws SlickException
95       *             Indicates a failure to load either file
96       */
97      public AngelCodeFont(String fntFile, Image image) throws SlickException {
98          fontImage = image;
99  
100         parseFnt(ResourceLoader.getResourceAsStream(fntFile));
101     }
102 
103     /**
104      * Create a new font based on a font definition from AngelCode's tool and
105      * the font image generated from the tool.
106      *
107      * @param fntFile
108      *            The location of the font defnition file
109      * @param imgFile
110      *            The location of the font image
111      * @throws SlickException
112      *             Indicates a failure to load either file
113      */
114     public AngelCodeFont(String fntFile, String imgFile) throws SlickException {
115         fontImage = new Image(imgFile);
116 
117         parseFnt(ResourceLoader.getResourceAsStream(fntFile));
118     }
119 
120     /**
121      * Create a new font based on a font definition from AngelCode's tool and
122      * the font image generated from the tool.
123      *
124      * @param fntFile
125      *            The location of the font defnition file
126      * @param image
127      *            The image to use for the font
128      * @param caching
129      *            True if this font should use display list caching
130      * @throws SlickException
131      *             Indicates a failure to load either file
132      */
133     public AngelCodeFont(String fntFile, Image image, boolean caching)
134             throws SlickException {
135         fontImage = image;
136         displayListCaching = caching;
137         parseFnt(ResourceLoader.getResourceAsStream(fntFile));
138     }
139 
140     /**
141      * Create a new font based on a font definition from AngelCode's tool and
142      * the font image generated from the tool.
143      *
144      * @param fntFile
145      *            The location of the font defnition file
146      * @param imgFile
147      *            The location of the font image
148      * @param caching
149      *            True if this font should use display list caching
150      * @throws SlickException
151      *             Indicates a failure to load either file
152      */
153     public AngelCodeFont(String fntFile, String imgFile, boolean caching)
154             throws SlickException {
155         fontImage = new Image(imgFile);
156         displayListCaching = caching;
157         parseFnt(ResourceLoader.getResourceAsStream(fntFile));
158     }
159 
160     /**
161      * Create a new font based on a font definition from AngelCode's tool and
162      * the font image generated from the tool.
163      *
164      * @param name
165      *            The name to assign to the font image in the image store
166      * @param fntFile
167      *            The stream of the font defnition file
168      * @param imgFile
169      *            The stream of the font image
170      * @throws SlickException
171      *             Indicates a failure to load either file
172      */
173     public AngelCodeFont(String name, @Nonnull InputStream fntFile, @Nonnull InputStream imgFile)
174             throws SlickException {
175         fontImage = new Image(imgFile, name, false);
176 
177         parseFnt(fntFile);
178     }
179 
180     /**
181      * Create a new font based on a font definition from AngelCode's tool and
182      * the font image generated from the tool.
183      *
184      * @param name
185      *            The name to assign to the font image in the image store
186      * @param fntFile
187      *            The stream of the font defnition file
188      * @param imgFile
189      *            The stream of the font image
190      * @param caching
191      *            True if this font should use display list caching
192      * @throws SlickException
193      *             Indicates a failure to load either file
194      */
195     public AngelCodeFont(String name, @Nonnull InputStream fntFile, @Nonnull InputStream imgFile,
196             boolean caching) throws SlickException {
197         fontImage = new Image(imgFile, name, false);
198 
199         displayListCaching = caching;
200         parseFnt(fntFile);
201     }
202 
203     /**
204      * Parse the font definition file
205      *
206      * @param fntFile
207      *            The stream from which the font file can be read
208      * @throws SlickException
209      */
210     private void parseFnt(@Nonnull InputStream fntFile) throws SlickException {
211         if (displayListCaching) {
212             baseDisplayListID = GL.glGenLists(DISPLAY_LIST_CACHE_SIZE);
213             if (baseDisplayListID == 0) displayListCaching = false;
214         }
215 
216         try {
217             // now parse the font file
218             BufferedReader in = new BufferedReader(new InputStreamReader(
219                     fntFile));
220             in.readLine();
221             String common = in.readLine();
222             ascent = parseMetric(common, "base="); //not used apparently ?
223             //ascent = parseMetric(common, "ascent=");
224             descent = parseMetric(common, "descent=");
225             parseMetric(common, "leading=");
226 
227             in.readLine();
228 
229             Map<Short, List<Short>> kerning = new HashMap<>(64);
230             List<Glyph> charDefs = new ArrayList<>(MAX_CHAR);
231             int maxChar = 0;
232             boolean done = false;
233             while (!done) {
234                 String line = in.readLine();
235                 if (line == null) {
236                     done = true;
237                 } else {
238                     if (line.startsWith("chars c")) {
239                         // ignore
240                     } else if (line.startsWith("char")) {
241                         Glyph def = parseChar(line);
242                         if (def != null) {
243                             maxChar = Math.max(maxChar, def.id);
244                             charDefs.add(def);
245                         }
246                     }
247                     if (line.startsWith("kernings c")) {
248                         // ignore
249                     } else if (line.startsWith("kerning")) {
250                         StringTokenizer tokens = new StringTokenizer(line, " =");
251                         tokens.nextToken(); // kerning
252                         tokens.nextToken(); // first
253                         short first = Short.parseShort(tokens.nextToken()); // first
254                                                                             // value
255                         tokens.nextToken(); // second
256                         int second = Integer.parseInt(tokens.nextToken()); // second
257                                                                             // value
258                         tokens.nextToken(); // offset
259                         int offset = Integer.parseInt(tokens.nextToken()); // offset
260                                                                             // value
261                         List<Short> values = kerning.get(first);
262                         if (values == null) {
263                             values = new ArrayList<>();
264                             kerning.put(first, values);
265                         }
266                         // Pack the character and kerning offset into a short.
267                         values.add((short) ((offset << 8) | second));
268                     }
269                 }
270             }
271 
272             chars = new Glyph[maxChar + 1];
273             for (Glyph def : charDefs) {
274                 chars[def.id] = def;
275             }
276 
277             // Turn each list of kerning values into a short[] and set on the
278             // chardef.
279             for (Entry<Short, List<Short>> entry : kerning.entrySet()) {
280                 short first = entry.getKey();
281                 List<Short> valueList = entry.getValue();
282                 short[] valueArray = new short[valueList.size()];
283                 for (int i=0; i<valueList.size(); i++)
284                     valueArray[i] = valueList.get(i);
285                 chars[first].kerning = valueArray;
286             }
287         } catch (IOException e) {
288             Log.error(e);
289             throw new SlickException("Failed to parse font file: " + fntFile);
290         }
291     }
292 
293     /**
294      * Returns the sprite sheet image that holds all of the images.
295      * @return the image for this bitmap font
296      */
297     public Image getImage() {
298         return fontImage;
299     }
300 
301     /**
302      * If a font has the same glyphs for upper and lower case text, we can
303      * minimize its glyph page size by only using one case. If single case
304      * is enabled (by default it is disabled), then the getGlyph method
305      * (and, in turn, glyph rendering/height/width/etc.) will try to find
306      * whichever case exists for the given code point (between 65-90 for
307      * upper case characters and 97-122 for lower case).
308      *
309      * @param enabled true to enable
310      */
311     public void setSingleCase(boolean enabled) {
312         this.singleCase = enabled;
313     }
314 
315     /**
316      * If a font has the same glyphs for upper and lower case text, we can
317      * minimize its glyph page size by only using one case. If single case
318      * is enabled (by default it is disabled), then the getGlyph method
319      * (and, in turn, glyph rendering/height/width/etc.) will try to find
320      * whichever case exists for the given code point (between 65-90 for
321      * upper case characters and 97-122 for lower case).
322      *
323      * @return true if single case is enabled
324      */
325     public boolean isSingleCase() {
326         return singleCase;
327     }
328 
329     private short parseMetric(@Nonnull String str, @Nonnull String sub) {
330         int ind = str.indexOf(sub);
331         if (ind!=-1) {
332             String subStr = str.substring(ind+sub.length());
333             ind = subStr.indexOf(' ');
334             return Short.parseShort(subStr.substring(0, ind!=-1 ? ind : subStr.length()));
335         }
336         return -1;
337     }
338 
339     /**
340      * Parse a single character line from the definition
341      *
342      * @param line
343      *            The line to be parsed
344      * @return The character definition from the line
345      * @throws SlickException Indicates a given character is not valid in an angel code font
346      */
347     @Nullable
348     private Glyph parseChar(String line) throws SlickException {
349         StringTokenizer tokens = new StringTokenizer(line, " =");
350 
351         tokens.nextToken(); // char
352         tokens.nextToken(); // id
353         short id = Short.parseShort(tokens.nextToken()); // id value
354         if (id < 0) {
355             return null;
356         }
357         if (id > MAX_CHAR) {
358             throw new SlickException("Invalid character '" + id
359                     + "': SpriteFont does not support characters above "
360                     + MAX_CHAR);
361         }
362 
363         tokens.nextToken(); // x
364         short x = Short.parseShort(tokens.nextToken()); // x value
365         tokens.nextToken(); // y
366         short y = Short.parseShort(tokens.nextToken()); // y value
367         tokens.nextToken(); // width
368         short width = Short.parseShort(tokens.nextToken()); // width value
369         tokens.nextToken(); // height
370         short height = Short.parseShort(tokens.nextToken()); // height value
371         tokens.nextToken(); // x offset
372         short xoffset = Short.parseShort(tokens.nextToken()); // xoffset value
373         tokens.nextToken(); // y offset
374         short yoffset = Short.parseShort(tokens.nextToken()); // yoffset value
375         tokens.nextToken(); // xadvance
376         short xadvance = Short.parseShort(tokens.nextToken()); // xadvance
377 
378         if (id != ' ') {
379             lineHeight = Math.max(height + yoffset, lineHeight);
380         }
381         Image img = fontImage.getSubImage(x, y, width, height);
382         return new Glyph(id, x, y, width, height, xoffset, yoffset, xadvance, img);
383     }
384 
385     /**
386      * @see org.newdawn.slick.Font#drawString(float, float, CharSequence)
387      */
388     public void drawString(float x, float y, @Nonnull CharSequence text) {
389         drawString(x, y, text, Color.white);
390     }
391 
392     /**
393      * @see org.newdawn.slick.Font#drawString(float, float, CharSequence,
394      *      org.newdawn.slick.Color)
395      */
396     public void drawString(float x, float y, @Nonnull CharSequence text, @Nonnull Color col) {
397         drawString(x, y, text, col, 0, text.length() - 1);
398     }
399 
400     /**
401      * @see Font#drawString(float, float, CharSequence, Color, int, int)
402      */
403     public void drawString(float x, float y, @Nonnull CharSequence text, @Nonnull Color col,
404             int startIndex, int endIndex) {
405         fontImage.bind();
406         col.bind();
407 
408         GL.glTranslatef(x, y, 0);
409         if (displayListCaching && startIndex == 0 && endIndex == text.length() - 1) {
410             DisplayList displayList = displayLists.get(text);
411             if (displayList != null) {
412                 GL.glCallList(displayList.id);
413             } else {
414                 // Compile a new display list.
415                 displayList = new DisplayList();
416                 displayList.text = text;
417                 int displayListCount = displayLists.size();
418                 if (displayListCount < DISPLAY_LIST_CACHE_SIZE) {
419                     displayList.id = baseDisplayListID + displayListCount;
420                 } else {
421                     displayList.id = eldestDisplayListID;
422                     displayLists.remove(eldestDisplayList.text);
423                 }
424 
425                 displayLists.put(text, displayList);
426 
427                 GL.glNewList(displayList.id, SGL.GL_COMPILE_AND_EXECUTE);
428                 render(text, startIndex, endIndex);
429                 GL.glEndList();
430             }
431         } else {
432             render(text, startIndex, endIndex);
433         }
434         GL.glTranslatef(-x, -y, 0);
435     }
436 
437     /**
438      * Render based on immediate rendering
439      *
440      * @param text The text to be rendered
441      * @param start The index of the first character in the string to render
442      * @param end The index of the last character in the string to render
443      */
444     private void render(@Nonnull CharSequence text, int start, int end) {
445         GL.glBegin(SGL.GL_QUADS);
446 
447         int x = 0, y = 0;
448         Glyph lastCharDef = null;
449 
450         for (int i = 0; i < text.length(); i++) {
451             char id = text.charAt(i);
452             if (id == '\n') {
453                 x = 0;
454                 y += getLineHeight();
455                 continue;
456             }
457             Glyph charDef = getGlyph(id);
458             if (charDef == null) {
459                 continue;
460             }
461             if (lastCharDef != null)
462                 x += lastCharDef.getKerning(id);
463             else
464                 x -= charDef.xoffset;
465 
466             lastCharDef = charDef;
467 
468             if ((i >= start) && (i <= end)) {
469                 charDef.image.drawEmbedded(x + charDef.xoffset, y + charDef.yoffset, charDef.width, charDef.height);
470 
471             }
472 
473             x += charDef.xadvance;
474         }
475         GL.glEnd();
476     }
477 
478     /**
479      * Returns the distance from the y drawing location to the top most pixel of the specified text.
480      *
481      * @param text
482      *            The text that is to be tested
483      * @return The yoffset from the y draw location at which text will start
484      */
485     public int getYOffset(@Nonnull String text) {
486         DisplayList displayList = null;
487         if (displayListCaching) {
488             displayList = displayLists.get(text);
489             if (displayList != null && displayList.yOffset != null) return displayList.yOffset.intValue();
490         }
491 
492         int stopIndex = text.indexOf('\n');
493         if (stopIndex == -1) stopIndex = text.length();
494 
495         int minYOffset = 10000;
496         for (int i = 0; i < stopIndex; i++) {
497             Glyph charDef = getGlyph(text.charAt(i));
498             if (charDef == null) {
499                 continue;
500             }
501             minYOffset = Math.min(charDef.yoffset, minYOffset);
502         }
503 
504         if (displayList != null) displayList.yOffset = (short) minYOffset;
505 
506         return minYOffset;
507     }
508 
509     /**
510      * @see org.newdawn.slick.Font#getHeight(CharSequence)
511      */
512     public int getHeight(@Nonnull CharSequence text) {
513         DisplayList displayList = null;
514         if (displayListCaching) {
515             displayList = displayLists.get(text);
516             if (displayList != null && displayList.height != null) return displayList.height.intValue();
517         }
518 
519         int lines = 0;
520         int maxHeight = 0;
521         for (int i = 0; i < text.length(); i++) {
522             char id = text.charAt(i);
523             if (id == '\n') {
524                 lines++;
525                 maxHeight = 0;
526                 continue;
527             }
528             // ignore space, it doesn't contribute to height
529             if (id == ' ') {
530                 continue;
531             }
532             Glyph charDef = getGlyph(id);
533             if (charDef == null) {
534                 continue;
535             }
536 
537             maxHeight = Math.max(charDef.height + charDef.yoffset,
538                     maxHeight);
539         }
540 
541         maxHeight += lines * getLineHeight();
542 
543         if (displayList != null) displayList.height = (short) maxHeight;
544 
545         return maxHeight;
546     }
547 
548     /**
549      * @see org.newdawn.slick.Font#getWidth(CharSequence)
550      */
551     public int getWidth(@Nonnull CharSequence text) {
552         DisplayList displayList = null;
553         if (displayListCaching) {
554             displayList = displayLists.get(text);
555             if (displayList != null && displayList.width != null) return displayList.width.intValue();
556         }
557 
558         int maxWidth = 0;
559         int width = 0;
560         Glyph lastCharDef = null;
561         for (int i = 0, n = text.length(); i < n; i++) {
562             char id = text.charAt(i);
563             if (id == '\n') {
564                 width = 0;
565                 continue;
566             }
567             Glyph charDef = getGlyph(id);
568             if (charDef == null) {
569                 continue;
570             }
571 
572             if (lastCharDef != null)
573                 width += lastCharDef.getKerning(id);
574 //            else //first glyph
575 //                width -= charDef.xoffset;
576 //
577             lastCharDef = charDef;
578 
579             //space characters have zero width, so use their xadvance instead
580             if (i < n - 1 || charDef.width==0) {
581                 width += charDef.xadvance;
582             } else {
583                 width += charDef.width + charDef.xoffset;
584             }
585             maxWidth = Math.max(maxWidth, width);
586         }
587         if (displayList != null) displayList.width = (short) maxWidth;
588 
589         return maxWidth;
590     }
591 
592     /**
593      * @see org.newdawn.slick.Font#getLineHeight()
594      */
595     public int getLineHeight() {
596         return lineHeight;
597     }
598 
599     /**
600      * Requires export from the newest version of Hiero. Alternatively, you
601      * could manually add <tt>descent=XX</tt> to the end of the 'common' line
602      * in your font file.
603      *
604      * The descent is the distance from the font's baseline to the
605      * bottom of most alphanumeric characters with descenders.
606      */
607     public int getDescent() {
608         return descent;
609     }
610 
611     /**
612      * Requires export from the newest version of Hiero. Alternatively, you
613      * could manually add <tt>ascent=XX</tt> to the end of the 'common' line
614      * in your font file.
615      *
616      * The ascent is the distance from the font's baseline to the top of most
617      * alphanumeric characters.
618      */
619     public int getAscent() {
620         return ascent;
621     }
622 
623     /**
624      * Returns the character definition for the given character.
625      *
626      * @param c the desired character
627      * @return the CharDef with glyph info
628      */
629     @Nullable
630     Glyph getGlyph(char c) {
631         Glyph g = c<0 || c>= chars.length ? null : chars[c];
632         if (g!=null)
633             return g;
634         if (singleCase) {
635             if (c>=65 && c<=90)
636                 c += 32;
637             else if (c>=97 && c<=122)
638                 c -= 32;
639         }
640         return c<0 || c>= chars.length ? null : chars[c];
641     }
642 
643     /**
644      * The definition of a single character as defined in the AngelCode file
645      * format
646      *
647      * @author kevin
648      */
649     public static class Glyph {
650         /** The id of the character */
651         public final short id;
652         /** The x location on the sprite sheet */
653         public final short x;
654         /** The y location on the sprite sheet */
655         public final short y;
656         /** The width of the character image */
657         public final short width;
658         /** The height of the character image */
659         public final short height;
660         /** The amount the x position should be offset when drawing the image */
661         public final short xoffset;
662         /** The amount the y position should be offset when drawing the image */
663         public final short yoffset;
664         /** The amount to move the current position after drawing the character */
665         public final short xadvance;
666         /** The sub-image containing the character */
667         public final Image image;
668         /** The display list index for this character */
669         protected short dlIndex;
670         /** The kerning info for this character */
671         short[] kerning;
672 
673         Glyph(short id, short x, short y, short width, short height,
674               short xoffset, short yoffset, short xadvance, Image image) {
675             this.id = id;
676             this.x = x;
677             this.y = y;
678             this.width = width;
679             this.height = height;
680             this.xoffset = xoffset;
681             this.yoffset = yoffset;
682             this.xadvance = xadvance;
683             this.image = image;
684         }
685 
686         /**
687          * @see java.lang.Object#toString()
688          */
689         @Nonnull
690         public String toString() {
691             return "[CharDef id=" + id + " x=" + x + " y=" + y + "]";
692         }
693 
694         /**
695          * Get the kerning offset between this character and the specified character.
696          * @param otherCodePoint The other code point
697          * @return the kerning offset
698          */
699         public int getKerning (int otherCodePoint) {
700             if (kerning == null) return 0;
701             int low = 0;
702             int high = kerning.length - 1;
703             while (low <= high) {
704                 int midIndex = (low + high) >>> 1;
705                 int value = kerning[midIndex];
706                 int foundCodePoint = value & 0xff;
707                 if (foundCodePoint < otherCodePoint)
708                     low = midIndex + 1;
709                 else if (foundCodePoint > otherCodePoint)
710                     high = midIndex - 1;
711                 else
712                     return value >> 8;
713             }
714             return 0;
715         }
716     }
717 
718     /**
719      * A descriptor for a single display list
720      *
721      * @author Nathan Sweet <misc@n4te.com>
722      */
723     static private class DisplayList {
724         /** The if of the distance list */
725         int id;
726         /** The offset of the line rendered */
727         Short yOffset;
728         /** The width of the line rendered */
729         Short width;
730         /** The height of the line rendered */
731         Short height;
732         /** The text that the display list holds */
733         CharSequence text;
734     }
735 }