View Javadoc
1   
2   package org.newdawn.slick.font;
3   
4   import java.awt.AlphaComposite;
5   import java.awt.Graphics2D;
6   import java.awt.RenderingHints;
7   import java.awt.font.FontRenderContext;
8   import java.awt.image.BufferedImage;
9   import java.awt.image.WritableRaster;
10  import java.nio.ByteBuffer;
11  import java.nio.ByteOrder;
12  import java.nio.IntBuffer;
13  import java.util.ArrayList;
14  import java.util.Iterator;
15  import java.util.List;
16  import java.util.ListIterator;
17  
18  import org.newdawn.slick.Color;
19  import org.newdawn.slick.Image;
20  import org.newdawn.slick.SlickException;
21  import org.newdawn.slick.UnicodeFont;
22  import org.newdawn.slick.font.effects.Effect;
23  import org.newdawn.slick.opengl.TextureImpl;
24  import org.newdawn.slick.opengl.renderer.Renderer;
25  import org.newdawn.slick.opengl.renderer.SGL;
26  
27  import javax.annotation.Nonnull;
28  
29  /**
30   * Stores a number of glyphs on a single texture.
31   * 
32   * @author Nathan Sweet <misc@n4te.com>
33   */
34  public class GlyphPage {
35      /** The interface to OpenGL */
36      private static final SGL GL = Renderer.get();
37  
38      /** The maxium size of an individual glyph */
39      public static final int MAX_GLYPH_SIZE = 256;
40  
41      /** A temporary working buffer */
42      private static final ByteBuffer scratchByteBuffer = ByteBuffer.allocateDirect(MAX_GLYPH_SIZE * MAX_GLYPH_SIZE * 4);
43  
44      static {
45          scratchByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
46      }
47      
48      /** A temporary working buffer */
49      @Nonnull
50      private static final IntBuffer scratchIntBuffer = scratchByteBuffer.asIntBuffer();
51      
52      
53      /** A temporary image used to generate the glyph page */
54      @Nonnull
55      private static final BufferedImage scratchImage = new BufferedImage(MAX_GLYPH_SIZE, MAX_GLYPH_SIZE, BufferedImage.TYPE_INT_ARGB);
56      /** The graphics context form the temporary image */
57      @Nonnull
58      private static final Graphics2D scratchGraphics = (Graphics2D)scratchImage.getGraphics();
59  
60      static {
61          scratchGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
62          scratchGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
63          scratchGraphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
64      }
65  
66      /** The render context in which the glyphs will be generated */
67      public static final FontRenderContext renderContext = scratchGraphics.getFontRenderContext();
68  
69      /**
70       * Get the scratch graphics used to generate the page of glyphs
71       *
72       * @return The scratch graphics used to build the page
73       */
74      @Nonnull
75      public static Graphics2D getScratchGraphics() {
76          return scratchGraphics;
77      }
78  
79      /** The font this page is part of */
80      private final UnicodeFont unicodeFont;
81      /** The width of this page's image */
82      private final int pageWidth;
83      /** The height of this page's image */
84      private final int pageHeight;
85      /** The image containing the glyphs */
86      @Nonnull
87      private final Image pageImage;
88      /** The x position of the page */
89      private int pageX;
90      /** The y position of the page */
91      private int pageY;
92      /** The height of the last row on the page */
93      private int rowHeight;
94      /** True if the glyphs are ordered */
95      private boolean orderAscending;
96      /** The list of glyphs on this page */
97      private final List<Glyph> pageGlyphs = new ArrayList<>(32);
98  
99      /**
100      * Create a new page of glyphs
101      *
102      * @param unicodeFont The font this page forms part of
103      * @param pageWidth The width of the backing texture.
104      * @param pageHeight The height of the backing texture.
105      * @throws SlickException if the backing texture could not be created.
106      */
107     public GlyphPage(UnicodeFont unicodeFont, int pageWidth, int pageHeight) {
108         this.unicodeFont = unicodeFont;
109         this.pageWidth = pageWidth;
110         this.pageHeight = pageHeight;
111 
112         //Note: since we use glTexSubImage2D instead of getGraphics, this doesn't need Image.createOffscreenGraphics
113         pageImage = new Image(pageWidth, pageHeight);
114     }
115 
116     /**
117      * Loads glyphs to the backing texture and sets the image on each loaded glyph. Loaded glyphs are removed from the list.
118      *
119      * If this page already has glyphs and maxGlyphsToLoad is -1, then this method will return 0 if all the new glyphs don't fit.
120      * This reduces texture binds when drawing since glyphs loaded at once are typically displayed together.
121      * @param glyphs The glyphs to load.
122      * @param maxGlyphsToLoad This is the maximum number of glyphs to load from the list. Set to -1 to attempt to load all the
123      *           glyphs.
124      * @return The number of glyphs that were actually loaded.
125      * @throws SlickException if the glyph could not be rendered.
126      */
127     public int loadGlyphs (@Nonnull List<Glyph> glyphs, int maxGlyphsToLoad) {
128         if (rowHeight != 0 && maxGlyphsToLoad == -1) {
129             // If this page has glyphs and we are not loading incrementally, return zero if any of the glyphs don't fit.
130             int testX = pageX;
131             int testY = pageY;
132             int testRowHeight = rowHeight;
133             for (Iterator<?> iter = getIterator(glyphs); iter.hasNext();) {
134                 Glyph glyph = (Glyph)iter.next();
135                 int width = glyph.getWidth();
136                 int height = glyph.getHeight();
137                 if (testX + width >= pageWidth) {
138                     testX = 0;
139                     testY += testRowHeight;
140                     testRowHeight = height;
141                 } else if (height > testRowHeight) {
142                     testRowHeight = height;
143                 }
144                 if (testY + testRowHeight >= pageWidth) return 0;
145                 testX += width;
146             }
147         }
148 
149         Color.white.bind();
150         pageImage.bind();
151 
152         int i = 0;
153         for (Iterator<?> iter = getIterator(glyphs); iter.hasNext();) {
154             Glyph glyph = (Glyph)iter.next();
155             int width = Math.min(MAX_GLYPH_SIZE, glyph.getWidth());
156             int height = Math.min(MAX_GLYPH_SIZE, glyph.getHeight());
157 
158             if (rowHeight == 0) {
159                 // The first glyph always fits.
160                 rowHeight = height;
161             } else {
162                 // Wrap to the next line if needed, or break if no more fit.
163                 if (pageX + width >= pageWidth) {
164                     if (pageY + rowHeight + height >= pageHeight) break;
165                     pageX = 0;
166                     pageY += rowHeight;
167                     rowHeight = height;
168                 } else if (height > rowHeight) {
169                     if (pageY + height >= pageHeight) break;
170                     rowHeight = height;
171                 }
172             }
173 
174             renderGlyph(glyph, width, height);
175             pageGlyphs.add(glyph);
176 
177             pageX += width;
178 
179             iter.remove();
180             i++;
181             if (i == maxGlyphsToLoad) {
182                 // If loading incrementally, flip orderAscending so it won't change, since we'll probably load the rest next time.
183                 orderAscending = !orderAscending;
184                 break;
185             }
186         }
187 
188         TextureImpl.bindNone();
189 
190         // Every other batch of glyphs added to a page are sorted the opposite way to attempt to keep same size glyps together.
191         orderAscending = !orderAscending;
192 
193         return i;
194     }
195 
196     /**
197      * Loads a single glyph to the backing texture, if it fits.
198      *
199      * @param glyph The glyph to be rendered
200      * @param width The expected width of the glyph
201      * @param height The expected height of the glyph
202      * @throws SlickException if the glyph could not be rendered.
203      */
204     private void renderGlyph(@Nonnull Glyph glyph, int width, int height) {
205         // Draw the glyph to the scratch image using Java2D.
206         scratchGraphics.setComposite(AlphaComposite.Clear);
207         scratchGraphics.fillRect(0, 0, MAX_GLYPH_SIZE, MAX_GLYPH_SIZE);
208         scratchGraphics.setComposite(AlphaComposite.SrcOver);
209         scratchGraphics.setColor(java.awt.Color.white);
210         for (Effect effect : unicodeFont.getEffects()) (effect).draw(scratchImage, scratchGraphics, unicodeFont, glyph);
211         glyph.setShape(null); // The shape will never be needed again.
212 
213         WritableRaster raster = scratchImage.getRaster();
214         int[] row = new int[width];
215         for (int y = 0; y < height; y++) {
216             raster.getDataElements(0, y, width, 1, row);
217             scratchIntBuffer.put(row);
218         }
219         GL.glTexSubImage2D(SGL.GL_TEXTURE_2D, 0, pageX, pageY, width, height, SGL.GL_BGRA, SGL.GL_UNSIGNED_BYTE,
220             scratchByteBuffer);
221         scratchIntBuffer.clear();
222 
223         glyph.setImage(pageImage.getSubImage(pageX, pageY, width, height));
224     }
225 
226     /**
227      * Returns an iterator for the specified glyphs, sorted either ascending or descending.
228      *
229      * @param glyphs The glyphs to return if present
230      * @return An iterator of the sorted list of glyphs
231      */
232     @Nonnull
233     private Iterator<Glyph> getIterator(@Nonnull List<Glyph> glyphs) {
234         if (orderAscending) return glyphs.iterator();
235         final ListIterator<Glyph> iter = glyphs.listIterator(glyphs.size());
236         return new Iterator<Glyph>() {
237             public boolean hasNext () {
238                 return iter.hasPrevious();
239             }
240 
241             public Glyph next () {
242                 return iter.previous();
243             }
244 
245             public void remove () {
246                 iter.remove();
247             }
248         };
249     }
250 
251     /**
252      * Returns the glyphs stored on this page.
253      *
254      * @return A list of {@link Glyph} elements on this page
255      */
256     @Nonnull
257     public List<Glyph> getGlyphs () {
258         return pageGlyphs;
259     }
260 
261     /**
262      * Returns the backing texture for this page.
263      *
264      * @return The image of this page of glyphs
265      */
266     @Nonnull
267     public Image getImage () {
268         return pageImage;
269     }
270 }