diff mde/resource/font.d @ 48:a98ffb64f066

Implemented font rendering (grayscale only; i.e. non-LCD). FontTexture creates a texture and caches glyphs. Font supports multiple styles/faces, loaded from config file (should probably be loaded via Options instead). TextBlock cache for glyph placement within a string. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 31 May 2008 12:40:26 +0100
parents 0fd51d2c6c8a
children bca7e2342d77
line wrap: on
line diff
--- a/mde/resource/font.d	Fri May 23 13:13:08 2008 +0100
+++ b/mde/resource/font.d	Sat May 31 12:40:26 2008 +0100
@@ -16,131 +16,243 @@
 /// Sets up freetype (in a basic way).
 module mde.resource.font;
 
+import mde.resource.FontTexture;
 import mde.resource.exception;
 
+import mde.mergetag.Reader;
+import mde.mergetag.DataSet;
+import mde.mergetag.exception;
+import mde.resource.paths;
+
 import derelict.freetype.ft;
 import derelict.opengl.gl;
 
+import tango.scrapple.text.convert.parseTo : parseTo;
 import tango.stdc.stringz;
+import Util = tango.text.Util;
 import tango.util.log.Log : Log, Logger;
 
+// "Publically import" this symbol:
+alias mde.resource.FontTexture.TextBlock TextBlock;
+
 private Logger logger;
 static this () {
     logger = Log.getLogger ("mde.resource.font");
 }
 
-/** Font class.
+/** FontStyle class.
  *
- * Particular to a font and size. (Maybe not size?)
+ * Particular to a font and size, and any other effects like bold/italic if ever implemented.
  * 
  * Note: it is not currently intended to be thread-safe. */
-class Font
+class FontStyle : IDataSection
 {
     //BEGIN Static: manager
     static {
-        /** Load the freetype library. */
-        void initialize () {
-            if (FT_Init_FreeType (&library))
-                throw new fontException ("error initialising the FreeType library");
+        debug void drawTexture() {
+            if (fontTex !is null)
+                fontTex.drawTexture;
         }
         
-        //FIXME: don't use GC for Font resources
+        /** Load the freetype library. */
+        void initialize () {
+            if (!confDir.exists (fileName))
+                throw new fontException ("No font settings file (fonts.[mtt|mtb])");
+            
+            if (FT_Init_FreeType (&library))
+                throw new fontException ("error initialising the FreeType library");
+            
+            // Check version
+            FT_Int maj, min, patch;
+            FT_Library_Version (library, &maj, &min, &patch);
+            if (maj != 2 || min != 3) {
+                char[128] tmp;
+                logger.warn (logger.format (tmp, "Using an untested FreeType version: {}.{}.{}", maj, min, patch));
+            }
+            
+            /* Load font settings
+             *
+             * Each mergetag section corresponds to a font; each is loaded whether used or not
+             * (however the actual font files are only loaded on use). A fallback id must be
+             * provided in the header which must match a loaded font name; if a non-existant font
+             * is requested a warning will be logged and this font returned. */
+            char[] fallbackName;
+            try {
+                IReader reader;
+                reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH, null, true);
+                reader.dataSecCreator = delegate IDataSection(ID id) {
+                    auto f = new FontStyle;
+                    fonts[id] = f;
+                    return f;
+                };
+                reader.read;
+                
+                // get fallback name
+                char[]* p = "fallback" in reader.dataset.header.Arg!(char[]).Arg;
+                if (p is null)
+                    throw new fontException ("No fallback font style specified");
+                fallbackName = *p;
+            }
+            catch (MTException e) {
+                throw new fontException ("Mergetag exception: "~e.msg);
+            }
+            
+            // Find the fallback
+            FontStyle* p = fallbackName in fonts;
+            if (p is null)
+                throw new fontException ("Fallback font style specified is not found");
+            fallback = *p;
+            // Load the fallback now, to ensure it's available.
+            // Also note that get() doesn't make sure the fallback is loaded before returning it.
+            fallback.load;
+        }
+        private const fileName = "fonts";
+        
+        //FIXME: don't use GC for FontStyle resources
         /** Cleanup: delete all fonts. */
         void cleanup () {
-            if (font)
-                delete font;
-            
             FT_Done_FreeType (library);
         }
         
-        /** Get a font.
-         *
-         * Later specify font/size.
-         *
-         * Throws:
-         *  fontLoadException when unable to load the font. */
-        Font get(char[] path) {
-            if (font is null) font = new Font(path);
-            return font;
+        /** Get a FontStyle instance, for a section in the fonts.mtt file.
+          *
+          * Also loads the font if it's not already loaded, so the first call may take some time.
+          *
+          * Uses fallback font-style if the desired style isn't known about or fails to load, so
+          * this function should never fail or throw, in theory (unless out of memory). The
+          * fallback should already be loaded. */
+        FontStyle get(char[] name) {
+            FontStyle* p = name in fonts;
+            if (p is null) {
+                logger.warn ("Font style "~name~" requested but not found; reverting to the fallback style.");
+                fonts[name] = fallback;	// set to prevent another warning getting logged
+                return fallback;
+            }
+            // Got it, but we need to make sure it's loaded:
+            try {
+                p.load;
+            } catch (Exception e) {
+                logger.warn ("Font style "~name~" failed to load; reverting to the fallback style.");
+                return fallback;
+            }
+            return *p;
         }
         
     private:
         FT_Library	library;
-        Font font;
+        FontTexture	fontTex;
+        FontStyle[ID]	fonts;		// all font styles known about; not necessarily loaded
+        FontStyle	fallback;	// used when requested font isn't in fonts
     }
     //END Static
     
+    this() {}
     
-    /** Load & cache a new font. */
-    this (char[] path)
+    //BEGIN Mergetag code
+    //NOTE: would it be better not to use a new mergetag file for this?
+    //FIXME: revise when gui can set options
+    void addTag (char[] tp, ID id, char[] dt) {
+        if (tp == "char[]") {
+            if (id == "path")
+                path = parseTo!(char[]) (dt);
+        }
+        else if (tp == "int") {
+            if (id == "size")
+                size = parseTo!(int) (dt);
+        }
+    }
+    void writeAll (ItemDelg) {}		// no writing the config for now
+    //END Mergetag code
+    
+    /** Load the font file.
+     *
+     * Even if the same font is used at multiple sizes, multiple copies of FT_Face are used.
+     * Sharing an FT_Face would require calling FT_Set_Pixel_Sizes each time a glyph is rendered or
+     * swapping the size information (face.size)? */
+    void load ()
     in {
         assert (library !is null, "font: library is null");
     } body {
         if (FT_New_Face (library, toStringz(path), 0, &face))
             throw new fontLoadException ("Unable to read font: "~path);
         
-        if (FT_Set_Pixel_Sizes (face, 0,16))
+        if (!FT_IS_SCALABLE (face))
+            throw new fontLoadException ("Currently no support for non-scalable fonts (which " ~
+                    path ~ " is). Please report if you want to see support.");
+        /* The following will need to be addressed when adding support for non-scalables:
+         *	Use of face.size.metrics.height property.
+         */
+        
+        if (FT_Set_Pixel_Sizes (face, 0,size))
             throw new fontLoadException ("Unable to set pixel size");
+        
+        // Create if necessary:
+        if (fontTex is null)
+            fontTex = new FontTexture;
+    }
+    
+    /** Update a TextBlock cache, as used by the textBlock function.
+     *
+     * The only use of this is to get the text block's size ahead of rendering, via TextBlock's w
+     * and h properties.
+     *
+     * This function will only actually update the cache if it is invalid, caused either by the
+     * font being changed or if cache.cacheVer < 0. */
+    void updateBlock (char[] str, ref TextBlock cache) {
+        fontTex.updateCache (face, str, cache);
     }
     
-    void drawStr (int x, int y, char[] str) {
-        FT_Vector pen = { x*64, y*64 };
-        auto g = face.glyph;
-        
-        FT_Matrix m;
-        m.xx = 0x10000;
-        m.xy = m.yx = 0;
-        m.yy = -0x10000;
-        
-        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
-        
-        FT_Pos y_adj = 0;	// y adjustment (for height)
-        
-        FT_Bool useKerning = FT_HAS_KERNING (face);
-        FT_UInt previous = 0;
-        
-        foreach (chr; str) {
-            auto gi = FT_Get_Char_Index (face, chr);
-            
-            if (useKerning && previous && gi)
-            {
-                FT_Vector  delta;
-
-
-                FT_Get_Kerning (face, previous, gi, FT_Kerning_Mode.FT_KERNING_DEFAULT, &delta);
-
-                pen.x += delta.x;
-            }
-            
-            FT_Set_Transform(face, &m, &pen);
-            if (FT_Load_Glyph(face, gi, FT_LOAD_RENDER))
-                return;	// give up
-            
-            if (y_adj < g.metrics.height) y_adj = g.metrics.height;
-            
-            auto b = g.bitmap;
-            if (b.pixel_mode != FT_Pixel_Mode.FT_PIXEL_MODE_GRAY || b.num_grays != 256) {
-                char[128] tmp;
-                logger.warn (logger.format (tmp,"Unsupported freetype bitmap format: {}, {}", b.pixel_mode, b.num_grays));
-                return;
-            }
-            if (b.pitch != b.width)
-                logger.info ("b.pitch != b.width");
-            
-            //NOTE: y direction!
-            glRasterPos2i (g.bitmap_left,g.bitmap_top /+ (y_adj >> 6)+/);
-            glDrawPixels (b.width, b.rows, GL_LUMINANCE, GL_UNSIGNED_BYTE, cast(void*) b.buffer);
-            
-            pen.x += g.advance.x;
-            pen.y += g.advance.y;
-            previous = gi;
+    /** Draw a block of text (may inlcude new-lines).
+     *
+     * As a CPU-side code optimisation, store a TextBlock (unique to str) and pass a reference as
+     * the cache argument. This is the recommended method, although for one-time calls when you
+     * don't need to know the size, the other version of textBlock may be used.
+     * ---------------------------------
+     * char[] str;
+     * TextBlock strCache;
+     * textBlock (x, y, str, strCache);
+     * ---------------------------------
+     * The TextBlock cache will be updated as necessary. Besides the initial update, this will only
+     * be if the font changes, or it is manually invalidated. This can be done by setting the
+     * TextBlock's cacheVer property to -1, which should be done if str is changed.
+     *
+     * The TextBlock's w and h properties are set to the size (in pixels) of the text block; other
+     * than this cache only serves as a small optimisation. However, the only way to get the size
+     * of a text block is to use a TextBlock cache and update it, either with this function or with
+     * the updateBlock function. */
+    void textBlock (int x, int y, char[] str, ref TextBlock cache) {
+        try {
+            fontTex.drawTextCache (face, str, cache, x, y);
+        } catch (Exception e) {
+            logger.warn ("Exception while drawing text: "~e.msg);
         }
     }
+    /** ditto */
+    void textBlock (int x, int y, char[] str) {
+        try {
+            // Using the cache method for one-time use is slightly less than optimal, but doing so
+            // isn't really recommended anyway (and maintaining two versions of fontTex.drawText
+            // would be horrible).
+            TextBlock cache;
+            fontTex.drawTextCache (face, str, cache, x, y);
+        } catch (Exception e) {
+            logger.warn ("Exception while drawing text: "~e.msg);
+        }
+    }
+    
+    
+    /** The font-specified vertical distance between the baseline of consecutive lines. */
+    int getLineSeparation () {
+        return face.size.metrics.height >> 6;
+    }
     
     ~this () {
         FT_Done_Face (face);
     }
     
 private:
+    char[]	path;	// path to font file
+    int		size;	// font size
+    
     FT_Face	face;
 }