Mercurial > projects > mde
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; }