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
25
26
27
28
29
30
31
32
33
34
35
36
37 public class AngelCodeFont implements Font {
38
39 private static final SGL GL = Renderer.get();
40
41
42
43
44
45 private static final int DISPLAY_LIST_CACHE_SIZE = 200;
46
47
48 private static final int MAX_CHAR = 255;
49
50
51 private boolean displayListCaching = true;
52
53
54 private Image fontImage;
55
56 private Glyph[] chars;
57
58 private int lineHeight;
59
60 private int baseDisplayListID = -1;
61
62 private int eldestDisplayListID;
63
64 private DisplayList eldestDisplayList;
65
66 private boolean singleCase = false;
67 private short ascent;
68 private short descent;
69
70
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
88
89
90
91
92
93
94
95
96
97 public AngelCodeFont(String fntFile, Image image) throws SlickException {
98 fontImage = image;
99
100 parseFnt(ResourceLoader.getResourceAsStream(fntFile));
101 }
102
103
104
105
106
107
108
109
110
111
112
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
122
123
124
125
126
127
128
129
130
131
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
142
143
144
145
146
147
148
149
150
151
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
162
163
164
165
166
167
168
169
170
171
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
182
183
184
185
186
187
188
189
190
191
192
193
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
205
206
207
208
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
218 BufferedReader in = new BufferedReader(new InputStreamReader(
219 fntFile));
220 in.readLine();
221 String common = in.readLine();
222 ascent = parseMetric(common, "base=");
223
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
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
249 } else if (line.startsWith("kerning")) {
250 StringTokenizer tokens = new StringTokenizer(line, " =");
251 tokens.nextToken();
252 tokens.nextToken();
253 short first = Short.parseShort(tokens.nextToken());
254
255 tokens.nextToken();
256 int second = Integer.parseInt(tokens.nextToken());
257
258 tokens.nextToken();
259 int offset = Integer.parseInt(tokens.nextToken());
260
261 List<Short> values = kerning.get(first);
262 if (values == null) {
263 values = new ArrayList<>();
264 kerning.put(first, values);
265 }
266
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
278
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
295
296
297 public Image getImage() {
298 return fontImage;
299 }
300
301
302
303
304
305
306
307
308
309
310
311 public void setSingleCase(boolean enabled) {
312 this.singleCase = enabled;
313 }
314
315
316
317
318
319
320
321
322
323
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
341
342
343
344
345
346
347 @Nullable
348 private Glyph parseChar(String line) throws SlickException {
349 StringTokenizer tokens = new StringTokenizer(line, " =");
350
351 tokens.nextToken();
352 tokens.nextToken();
353 short id = Short.parseShort(tokens.nextToken());
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();
364 short x = Short.parseShort(tokens.nextToken());
365 tokens.nextToken();
366 short y = Short.parseShort(tokens.nextToken());
367 tokens.nextToken();
368 short width = Short.parseShort(tokens.nextToken());
369 tokens.nextToken();
370 short height = Short.parseShort(tokens.nextToken());
371 tokens.nextToken();
372 short xoffset = Short.parseShort(tokens.nextToken());
373 tokens.nextToken();
374 short yoffset = Short.parseShort(tokens.nextToken());
375 tokens.nextToken();
376 short xadvance = Short.parseShort(tokens.nextToken());
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
387
388 public void drawString(float x, float y, @Nonnull CharSequence text) {
389 drawString(x, y, text, Color.white);
390 }
391
392
393
394
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
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
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
439
440
441
442
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
480
481
482
483
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
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
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
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
575
576
577 lastCharDef = charDef;
578
579
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
594
595 public int getLineHeight() {
596 return lineHeight;
597 }
598
599
600
601
602
603
604
605
606
607 public int getDescent() {
608 return descent;
609 }
610
611
612
613
614
615
616
617
618
619 public int getAscent() {
620 return ascent;
621 }
622
623
624
625
626
627
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
645
646
647
648
649 public static class Glyph {
650
651 public final short id;
652
653 public final short x;
654
655 public final short y;
656
657 public final short width;
658
659 public final short height;
660
661 public final short xoffset;
662
663 public final short yoffset;
664
665 public final short xadvance;
666
667 public final Image image;
668
669 protected short dlIndex;
670
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
688
689 @Nonnull
690 public String toString() {
691 return "[CharDef id=" + id + " x=" + x + " y=" + y + "]";
692 }
693
694
695
696
697
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
720
721
722
723 static private class DisplayList {
724
725 int id;
726
727 Short yOffset;
728
729 Short width;
730
731 Short height;
732
733 CharSequence text;
734 }
735 }