/ContentSystem/Rendering/FontData.cs
C# | 1441 lines | 826 code | 144 blank | 471 comment | 90 complexity | 36f42db1212527fad6e6864f395aab3a MD5 | raw file
Possible License(s): Apache-2.0
- using System;
- using System.Collections.Generic;
- using Delta.ContentSystem.Rendering.Helpers;
- using Delta.ContentSystem.Xml;
- using Delta.Engine;
- using Delta.Utilities;
- using Delta.Utilities.Datatypes;
- using Delta.Utilities.Datatypes.Advanced;
- using Delta.Utilities.Graphics;
- using Delta.Utilities.Helpers;
- using Delta.Utilities.Xml;
- using NUnit.Framework;
-
- namespace Delta.ContentSystem.Rendering
- {
- /// <summary>
- /// That class takes care about the correct loading of the requested bitmap
- /// font based on the given parameters in the constructor. The font bitmap
- /// textures itself (font maps) will be created by the FontGenerator of the
- /// ContentServer.
- /// </summary>
- public class FontData : XmlData
- {
- #region Constants
- /// <summary>
- /// The minimal size (in points) that a font is allowed to have.
- /// </summary>
- public const int MinFontSize = 5;
-
- /// <summary>
- /// The maximal size (in points) that a font is allowed to have.
- /// </summary>
- public const int MaxFontSize = 72;
-
- /// <summary>
- /// Helper constant to initialize the "char" variables
- /// </summary>
- private const char NoChar = '\0';
-
- /// <summary>
- /// The fallback character which is used if a character isn't supported by
- /// a font.
- /// </summary>
- /// <remarks>
- /// Is used for parsing a text.
- /// </remarks>
- private const char FallbackChar = '?';
- #endregion
-
- #region Get (Static)
- /// <summary>
- /// Get and load content based on the content name. This method makes sure
- /// we do not load the same content twice (the constructor is protected).
- /// </summary>
- /// <param name="contentName">
- /// Content name we want to load, this is passed onto the Content System,
- /// which will do the actual loading with help of the Load method in this
- /// class.
- /// </param>
- /// <returns>
- /// The loaded Content object, always unique for the same name, this helps
- /// comparing data.
- /// </returns>
- public new static FontData Get(string contentName)
- {
- return Get<FontData>(contentName, ContentType.Font);
- }
-
- /// <summary>
- /// Get or load a font without the content name, but instead by searching
- /// for all the meta data: Font Family Name, Font Size and Font Style.
- /// Please note that other values like Font Tracking is ignored, you have
- /// to use the Get(contentName) overload for that.
- /// <para />
- /// This method will always return a font. If just a style or size was not
- /// found a content entry with the same font family is returned. If that
- /// is also not found the default font is returned (which is part of each
- /// project because the Engine content is always available as fallback
- /// content).
- /// </summary>
- /// <param name="fontFamilyName">
- /// Font family name (like "Verdana", "Arial", etc.).
- /// </param>
- /// <param name="fontSize">Font size (like 9pt, 12pt, 24pt, etc.).</param>
- /// <param name="fontStyle">Font style flags (bold, italic, sharp, etc.)
- /// </param>
- /// <returns>
- /// The loaded FontData content object. If this content was loaded before
- /// the same copy is used again.
- /// </returns>
- public static FontData Get(string fontFamilyName, int fontSize,
- FontStyle fontStyle)
- {
- ContentMetaData bestMatch = null;
- Dictionary<string, ContentMetaData> allFontContent =
- ContentManager.GetAllContentMetaData(ContentType.Font);
- foreach (ContentMetaData fontContent in allFontContent.Values)
- {
- // Note: ShaderFlags is used for ShaderFeatureFlags.
- if (fontContent.FontFamilyName.Compare(fontFamilyName))
- {
- // Did we find a matching font with the correct size and style?
- if (fontContent.FontSize == fontSize &&
- fontContent.FontStyle == fontStyle)
- {
- // Thats good. Get out of here and load the FontData!
- return Get(fontContent.Name);
- }
-
- // Just name matches, then only set this if we have nothing yet.
- if (bestMatch == null)
- {
- bestMatch = fontContent;
- }
- // If the font size matches too, replace the entry.
- if (fontContent.FontSize == fontSize)
- {
- if (bestMatch.FontSize != fontSize)
- {
- bestMatch = fontContent;
- }
- }
- }
- } // foreach
-
- // Did we find something? Then load that, but report that this is not
- // a perfect match.
- if (bestMatch != null)
- {
- Log.Warning(
- "Found a font with the font family name '" + fontFamilyName +
- "', but the size='" + fontSize + "' or style='" + fontStyle +
- "' did not match: " + bestMatch);
- // Return and load it anyway.
- return Get(bestMatch.Name);
- }
-
- Log.Warning(
- "Found no font with the font family name '" + fontFamilyName +
- "' (in any size, but the size='" + fontSize + "' and style='" +
- fontStyle + "' was requested).");
-
- // Nothing found? Then load the fallback font (will throw more warnings)!
- return Get("");
- }
- #endregion
-
- #region Default (Static)
- /// <summary>
- /// The font data which is chosen by default if a concrete one is not
- /// available (yet). This is always Verdana, 12pt, with an outline.
- /// </summary>
- public static FontData Default
- {
- get
- {
- if (defaultData == null)
- {
- defaultData = Get("Verdana", 12, FontStyle.AddOutline);
- } // if
-
- return defaultData;
- } // get
- }
- #endregion
-
- #region ResolutionFonts (Public)
- /// <summary>
- /// Font content entries can either be concrete font settings with an
- /// xml file for all the glyph data or just a parent entry with children
- /// that contain the resolution specific font data (4 children for the 4
- /// default resolutions, see Font.DetermineBestFont, which switches fonts
- /// at 320p, 480p, 640p and 960p).
- /// </summary>
- public FontData[] ResolutionFonts;
- #endregion
-
- #region FamilyName (Public)
- /// <summary>
- /// The family name of the font, e.g. Verdana, Consolas, etc.
- /// </summary>
- public string FamilyName
- {
- get
- {
- return data.FontFamilyName;
- }
- }
- #endregion
-
- #region SizeInPoints (Public)
- /// <summary>
- /// The size of the font in points, e.g. 12, 14, etc.
- /// </summary>
- public int SizeInPoints
- {
- get
- {
- return data.FontSize;
- }
- }
- #endregion
-
- #region Style (Public)
- /// <summary>
- /// The style how the characters will be shown, e.g. normal, bold, italic,
- /// etc.
- /// </summary>
- public FontStyle Style
- {
- get
- {
- return data.FontStyle;
- }
- }
- #endregion
-
- #region Tracking (Public)
- /// <summary>
- /// Defines the distance (in normalized percentage) which is added between
- /// 2 characters. This is only used for this FontData and set by the
- /// content system (cannot be changed by the user, unlike
- /// Font.TrackingMultiplier, which is just a multiplier to adjust spacing).
- /// <para/>
- /// 0.0 means no additional space (default)
- /// <para/>
- /// 1.0 means 100% of the AdvanceWidth of the character as additional space
- /// to the right.
- /// <para/>
- /// -1.0 means 100% of the AdvanceWidth of the character as "reverse" space
- /// to the left.
- /// </summary>
- public float Tracking
- {
- get
- {
- return data.FontTracking / 100.0f;
- }
- }
- #endregion
-
- #region PixelLineHeight (Public)
- /// <summary>
- /// The height of the font in pixel space based on the set font size.
- /// <para/>
- /// CAUTION: That is not the final height of the font on the screen,
- /// it is multiplied by the font meta data line height multiplier and then
- /// converted to quad space for rendering (see Font class). Finally it can
- /// also be modified by the Font class with the LineHeightMultiplier.
- /// </summary>
- public int PixelLineHeight
- {
- get;
- private set;
- }
- #endregion
-
- #region LeftOffset
- /// <summary>
- /// When each font character glyph is drawn by the FontGenerator several
- /// factors might offset the character like the distance from the baseline
- /// to the actual first pixel drawn and also margins added and effects
- /// like outlining and adding shadow might change the render size. In oder
- /// to still position the character to render correctly this offset must be
- /// added to the render position to correctly position each glyph when
- /// rendering a font.
- /// </summary>
- public int LeftOffset
- {
- get;
- private set;
- }
- #endregion
-
- #region TopOffset
- /// <summary>
- /// Similar to RenderMarginLeft each glyph might have been offseted because
- /// of margins used in FontGenerator plus outlining and shadow effects.
- /// In oder to still position the character to render correctly this offset
- /// must be added to the render position to correctly position each glyph
- /// when rendering a font.
- /// </summary>
- public int TopOffset
- {
- get;
- private set;
- }
- #endregion
-
- #region FontMapNames (Public)
- /// <summary>
- /// The list of the names of font maps where all characters are stored.
- /// </summary>
- public string[] FontMapNames
- {
- get;
- private set;
- }
- #endregion
-
- #region Internal
-
- #region glyphDictionary (Internal)
- /// <summary>
- /// Dictionary of each unicode character we have in the font maps and can
- /// use for drawing.
- /// </summary>
- internal Dictionary<char, Glyph> glyphDictionary;
- #endregion
-
- #endregion
-
- #region Private
-
- #region defaultData (Private)
- /// <summary>
- /// Default data
- /// </summary>
- private static FontData defaultData;
- #endregion
-
- #region kernDictionary (Private)
- /// <summary>
- /// Character spacing with kerning dictionary, which tells us how much
- /// spacing is needed between two characters. Most character combinations
- /// have no extra values in here, but some have (like W, I, etc.)
- /// </summary>
- private Dictionary<char, Dictionary<char, int>> kernDictionary;
- #endregion
-
- #endregion
-
- #region Constructors
- /// <summary>
- /// Creates a font from the content data. All font data is stored in xml
- /// files (in the Fonts content directory). Will also load all bitmaps
- /// required for this font.
- /// </summary>
- /// <param name="contentName">Font content name to load. If this is empty
- /// no content will be loaded (just fallback data will be set).</param>
- protected FontData(string contentName)
- // For the content we need to make sure that each font content name
- // is unique and therefore we need to add the size, etc. to the name!
- : base(contentName, ContentType.Font)
- {
- }
- #endregion
-
- #region GetGlyphDrawInfos (Public)
- /// <summary>
- /// Return the draw info of all glyphs that are needed to show the text
- /// on the screen (in pixel space).
- /// <para/>
- /// ASCII reference:
- /// http://www.tcp-ip-info.de/tcp_ip_und_internet/ascii.htm
- /// <para/>
- /// Note: The glyph info exists as single array but the data itself
- /// represents the text inclusive line breaks, so in other words the
- /// several text lines are "flattened" to a single-dimensioned array.
- /// </summary>
- /// <param name="text">Text</param>
- /// <param name="lineSpacing">Line spacing</param>
- /// <param name="textAlignment">Horizontal text alignment mode</param>
- /// <returns>
- /// List of GlyphDrawInfos ready for drawing.
- /// </returns>
- public GlyphDrawInfo[] GetGlyphDrawInfos(string text, float lineSpacing,
- HorizontalAlignment textAlignment)
- {
- Size infiniteSize = Size.Zero;
- return GetGlyphDrawInfos(text, lineSpacing, textAlignment, false, false,
- ref infiniteSize);
- }
-
- /// <summary>
- /// Return the draw info of all glyphs that are needed to show the text
- /// on the screen (in pixel space).
- /// <para/>
- /// ASCII reference:
- /// http://www.tcp-ip-info.de/tcp_ip_und_internet/ascii.htm
- /// <para/>
- /// Note: The glyph info exists as single array but the data itself
- /// represents the text inclusive line breaks, so in other words the
- /// several text lines are "flattened" to a single-dimensioned array.
- /// </summary>
- /// <param name="text">Text</param>
- /// <param name="lineSpacing">Line spacing</param>
- /// <param name="textAlignment">Horizontal text alignment mode</param>
- /// <param name="isClippingOn">Is clipping check required</param>
- /// <param name="maxTextSize">Max. available size in Pixel Space</param>
- /// <param name="isWordWrapOn">
- /// Indicates if the words of the given text should be wrapped or clipped
- /// (if enabled) at the end of a text line.
- /// </param>
- /// <returns>
- /// List of GlyphDrawInfos ready for drawing.
- /// </returns>
- public GlyphDrawInfo[] GetGlyphDrawInfos(string text, float lineSpacing,
- HorizontalAlignment textAlignment, bool isClippingOn, bool isWordWrapOn,
- ref Size maxTextSize)
- {
- if (FontMapNames.Length == 0)
- {
- // This warning is kind of a duplicate because we already got a warning
- // from the font data loading, but this one is useful because it
- // happens when trying to render some text, which will be aborted!
- return new GlyphDrawInfo[0];
- } // if
-
- // Define the list of glyphs which represents the drawing info for
- // each character of the text
- List<GlyphDrawInfo> glyphInfos = new List<GlyphDrawInfo>();
-
- // At first split the given text in several text lines
- // (if there are line breaks)
- List<float> textlineWidths;
- float maxTextlineWidth;
- List<List<char>> textlines = GetTextLines(text, lineSpacing, maxTextSize,
- textAlignment, isClippingOn, isWordWrapOn,
- out textlineWidths, out maxTextlineWidth);
-
- // The total height for every line
- float finalLineHeight = PixelLineHeight * lineSpacing;
- // The values of the drawing info of the last character
- GlyphDrawInfo lastDrawInfo = new GlyphDrawInfo
- {
- DrawArea = new Rectangle(Point.Zero, new Size(0, finalLineHeight)),
- UV = new Rectangle(Point.Zero, new Size(0, finalLineHeight)),
- };
-
- for (int lineId = 0; lineId < textlines.Count; lineId++)
- {
- List<char> textline = textlines[lineId];
-
- // Sanity check, because we only need to align and handle characters
- // if we have some
- if (textline.Count > 0)
- {
- switch (textAlignment)
- {
- // For left-aligned texts (that goes left-to-right) we need to
- // subtract the LeftSideBearing to make sure that every first
- // character of a text line starts really at the same left border
- // -> 'abcde' instead of 'abcde'
- // 'bcd' ' bcd'
- // 'cdef' 'cdef'
- case HorizontalAlignment.Left:
- char firstChar = textline[0];
- lastDrawInfo.DrawArea.X = MathHelper.Round(
- glyphDictionary[firstChar].LeftSideBearing);
- break;
-
- // For center-aligned text, we add here the LeftSideBearing too
- // like in the left-aligned mode, but additionally we still make
- // sure that the every text line is centered (based on the
- // longest text line)
- case HorizontalAlignment.Centered:
- firstChar = textline[0];
- lastDrawInfo.DrawArea.X = MathHelper.Round(
- (maxTextlineWidth - textlineWidths[lineId]) * 0.5f +
- glyphDictionary[firstChar].LeftSideBearing);
- break;
-
- // For right-aligned texts (that goes left-to-right) we need to
- // add the RightSideBearing of the last character of a text line
- // at the beginning the text line to make sure that every last
- // character of a text line ends really at the same right border
- // (because we all add the chars from left-to-right)
- // -> 'abcde' instead of 'abcde'
- // 'bcd' 'bcd '
- // 'cdef' 'cdef'
- case HorizontalAlignment.Right:
- char lastChar = textline[textline.Count - 1];
- lastDrawInfo.DrawArea.X = MathHelper.Round(
- (maxTextlineWidth - textlineWidths[lineId]) -
- glyphDictionary[lastChar].RightSideBearing);
- break;
-
- default:
- lastDrawInfo.DrawArea.X = 0;
- // Nothing to do for us
- break;
- } // switch
-
- // Also apply the offset for each letter
- lastDrawInfo.DrawArea.X -= LeftOffset;
- lastDrawInfo.DrawArea.Y -= TopOffset;
-
- // Now iterate through all characters of the text
- for (int charIndex = 0; charIndex < textline.Count; charIndex++)
- {
- char textChar = textline[charIndex];
-
- // Figure out which is the next character, because we need it for
- // the kerning to it
- char nextChar =
- charIndex + 1 < textline.Count
- ? textline[charIndex + 1]
- : NoChar;
-
- // Get now the character we have to handle here.
- // Note: We don't need to care about sanity checks, because that
- // has happen in the "GetTextlines()" already which means that we
- // get here only supported characters
- Glyph characterGlyph;
- glyphDictionary.TryGetValue(textChar, out characterGlyph);
- if (characterGlyph == null)
- {
- Log.Warning(
- "The glyph for the character '" + textChar +
- "' doesn't exists yet in the FontData '" + Name + "', will " +
- "use the '?' glyph instead.", false);
- characterGlyph = glyphDictionary['?'];
- } // if
-
- // Get the width of the current glyph that is used know the
- // required distance to the next character
- int glyphWidth = characterGlyph.GetDrawWidth(nextChar, Tracking);
-
- // and build the drawing info for the current character now
- GlyphDrawInfo newDrawInfo = new GlyphDrawInfo
- {
- DrawArea = new Rectangle(lastDrawInfo.DrawArea.TopLeft,
- characterGlyph.UV.Size),
- UV = characterGlyph.FontMapUV,
- FontMapId = characterGlyph.FontMapId,
- };
-
- // that we add the glyph info into the list
- glyphInfos.Add(newDrawInfo);
- // and update the last glyph drawing info with it
- lastDrawInfo = newDrawInfo;
- // with the position for the next character
- lastDrawInfo.DrawArea.X += glyphWidth;
- } // for
- } // if
-
- // Go to the next text line
- lastDrawInfo.DrawArea.Y += finalLineHeight;
- } // for
-
- return glyphInfos.ToArray();
- }
- #endregion
-
- #region ToString (Public)
- /// <summary>
- /// To String method, will just extend the Content.ToString method by
- /// some extra font meta information.
- /// </summary>
- /// <returns>A info string about this object instance.</returns>
- public override string ToString()
- {
- return base.ToString() + ", Font Family Name=" + data.FontFamilyName +
- ", Font Size=" + data.FontSize + ", Font Style=" + data.FontStyle;
- }
- #endregion
-
- #region Methods (Private)
-
- #region Load
- /// <summary>
- /// Native load method, will just load the xml data.
- /// </summary>
- /// <param name="alreadyLoadedNativeData">
- /// The first instance that has already loaded the required content data
- /// of this content class or just 'null' if there is none loaded yet (or
- /// anymore).
- /// </param>
- protected override void Load(Content alreadyLoadedNativeData)
- {
- // Make sure FontMapNames is not null, else we might crash later.
- FontMapNames = new string[0];
-
- // If this entry has no RelativeFilePath, it should have children.
- if (String.IsNullOrEmpty(RelativeFilePath))
- {
- // Just load all the children entries (only up to 4, ignore the rest)
- List<FontData> childFonts = new List<FontData>();
- foreach (ContentMetaData child in data.Children)
- {
- // Grab all Font children content entries
- if (child.Type == ContentType.Font)
- {
- childFonts.Add(Get(child.Name));
- }
- }
-
- // If we have none, there is something wrong with the content!
- if (childFonts.Count == 0)
- {
- // Only warn if the application is still running and was not aborted.
- if (Application.IsShuttingDown == false)
- {
- Log.Warning(
- "Unable to load font data, because the font content '" +
- Name + "' has no file data and also no font children!", false);
- }
- return;
- }
-
- // Warnings if we have too many fonts are in the Font constructor!
- ResolutionFonts = childFonts.ToArray();
- // We don't need to load anything more
- return;
- } // if
-
- // First load all the xml font data.
- base.Load(alreadyLoadedNativeData);
-
- // If that failed, we can't do anything, font rendering won't work.
- if (RootNode == null ||
- String.IsNullOrEmpty(RootNode.Name))
- {
- Log.Warning(
- "Unable to load font data '" + Name + "', because the content " +
- "couldn't be found and it seems that there is no working fallback " +
- "either. Tried to load file: " + RelativeFilePath, false);
- return;
- } // if
-
- // Okay, do all the loading now!
- try
- {
- #region Validation
- // First find out if this is a font at all
- if (RootNode.Name != "Font")
- {
- // Note: Exceptions are handled below. This will not crash caller.
- Log.Warning(
- "This content file '" + RootNode.FilePath + "' is not a font, " +
- "unable to load the font data! (RootNode=" + RootNode.Name + ")",
- false);
- return;
- } // if
- #endregion
-
- // Grab the version to determinate what data we can expect.
- Version version = new Version(0, 9, 0);
- string versionText = RootNode.GetAttribute("Version");
- if (String.IsNullOrEmpty(versionText) == false)
- {
- Version.TryParse(versionText, out version);
- }
-
- string bitmapNameAttribute = "Name";
- string characterAttribute = "Character";
- string bitmapIndexAttribute = "BitmapIndex";
- string uvAttribute = "UV";
- string advanceWidthAttribute = "AdvanceWidth";
- string leftBearingAttribute = "LeftBearing";
- string rightBearingAttribute = "RightBearing";
- string firstAttribute = "First";
- string secondAttribute = "Second";
- string distanceAttribute = "Distance";
-
- // Still support fonts from v0.9.0, which used the version 0.1. Skip
- // all the validation checks for v0.1
- if (version > new Version(0, 1))
- {
- // Load some global data for this font and validate in debug mode
- LeftOffset = RootNode.GetAttributeAs("LeftOffset", 0);
- TopOffset = RootNode.GetAttributeAs("TopOffset", 0);
- }
- else
- {
- // The old version had slightly different names for some attributes
- bitmapNameAttribute = "Filename";
- characterAttribute = "char";
- uvAttribute = "uv";
- advanceWidthAttribute = "advanceWidth";
- leftBearingAttribute = "leftBearing";
- rightBearingAttribute = "rightBearing";
- bitmapIndexAttribute = "bitmap";
- firstAttribute = "first";
- secondAttribute = "second";
- distanceAttribute = "distance";
- }
-
- PixelLineHeight = RootNode.GetAttributeAs("LineHeight", SizeInPoints);
- // and make sure we have no other data loaded yet
- glyphDictionary = new Dictionary<char, Glyph>();
- kernDictionary = new Dictionary<char, Dictionary<char, int>>();
-
- List<string> fontMapNames = new List<string>();
- // Little helper to remember bitmap sizes, which are also known
- // by the material once it is loaded, but to keep delayed loading
- // we do not need to load it here right away (only when it is used
- // for the first time and we actually need the material for drawing).
- List<Size> fontMapSizes = new List<Size>();
- // Helper to make xml attribute warnings more descriptive.
- string fontWarningText = "Font " + Name;
-
- // Get the used bitmaps, glyphs and kernings, thats all we need!
- foreach (XmlNode childNode in RootNode.Children)
- {
- switch (childNode.Name)
- {
- #region 1. step - load all fontmaps
- case "Bitmap":
- // Load the font map data, currently we only support a single one
- string fontMapName = childNode.GetAttribute(bitmapNameAttribute);
- int fontMapWidth = childNode.GetAttributeAs("Width", 1);
- int fontMapHeight = childNode.GetAttributeAs("Height", 1);
- fontMapNames.Add(fontMapName);
- fontMapSizes.Add(new Size(fontMapWidth, fontMapHeight));
- break;
- #endregion
-
- #region 2. step - load all glyphs
- case "Glyphs":
- // Load all glyphs
- foreach (XmlNode glyphNode in childNode.Children)
- {
- #region Validation
- // We cannot load glyphs without having font maps!
- if (fontMapNames.Count == 0)
- {
- throw new NotSupportedException(
- "We cannot add and calculate font glyphs with no bitmap " +
- "fonts defined. The font xml data is not complete!");
- } // if
- #endregion
-
- char character =
- glyphNode.GetAttributeAs(characterAttribute, ' ');
- Glyph glyph = new Glyph
- {
- FontMapId =
- glyphNode.GetAttributeAs(bitmapIndexAttribute, 0),
- UV = Rectangle.FromColladaString(
- glyphNode.GetAttribute(uvAttribute)),
- LeftSideBearing = (int)Math.Round(glyphNode.GetAttributeAs(
- leftBearingAttribute, 0.0f, fontWarningText)),
- RightSideBearing = (int)Math.Round(glyphNode.GetAttributeAs(
- rightBearingAttribute, 0.0f, fontWarningText)),
- Kernings = null,
- };
-
- if (glyph.FontMapId >=
- fontMapNames.Count)
- {
- Log.Warning(
- "Unable to use FontMapNumber '" + glyph.FontMapId +
- "' for this glyph because we only have '" +
- fontMapNames.Count + "' font maps. The FontMapNumber " +
- "will resseted now to '" + (fontMapNames.Count - 1) + "'");
- glyph.FontMapId = fontMapNames.Count - 1;
- } // if
-
- // Default fallback is width minus some small offset,
- // this will never be used except advanceWidth is missing
- glyph.AdvanceWidth =
- glyphNode.GetAttributeAs(advanceWidthAttribute,
- glyph.UV.Width - 2.0f, fontWarningText);
-
- // Calculate the FontMapUV with help of font map sizes
- // remembered above. Error checking is done above already.
- Size fontMapSize = fontMapSizes[glyph.FontMapId];
- // Build the UV's with the usual halfpixel offset, etc.
- glyph.FontMapUV = Rectangle.BuildUVRectangle(glyph.UV,
- fontMapSize);
-
- // And finally add it to our glyph dictionary
- glyphDictionary.Add(character, glyph);
- } // foreach
- break;
- #endregion
-
- #region 3. step - load all kernings
- case "Kernings":
- // Load all kernings
- foreach (XmlNode kernNode in childNode.Children)
- {
- char firstChar = kernNode.GetAttributeAs(firstAttribute, ' ',
- fontWarningText);
- char secondChar = kernNode.GetAttributeAs(secondAttribute, ' ',
- fontWarningText);
- int kerningDistance = kernNode.GetAttributeAs(distanceAttribute,
- 0, fontWarningText);
-
- // Try to find the character for that a kerning with an other
- // exists
- Glyph glyph;
- if (glyphDictionary.TryGetValue(firstChar, out glyph))
- {
- // If it exists (what should be usually the case)
- Dictionary<char, int> glyphKernings;
- // look if we have already added some kernings
- if (glyph.Kernings != null)
- {
- glyphKernings = glyph.Kernings;
- } // if
-
- // or do we add the first one
- else
- {
- // in that case initialize the dictionary
- glyphKernings = new Dictionary<char, int>();
- // and link it to the glyph
- glyph.Kernings = glyphKernings;
- } // else
-
- // finally still add the new distance value
- glyphKernings.Add(secondChar, kerningDistance);
- } // if
- else
- {
- Log.Warning(
- "The font content data contains a kerning for the " +
- "character '" + firstChar + "' to the character '" +
- secondChar + "' but there is no glyph information for '" +
- firstChar + "'.", false);
- } // else
- } // foreach
- break;
- #endregion
- } // switch
- } // foreach
-
- FontMapNames = fontMapNames.ToArray();
- } // try
- catch (Exception ex)
- {
- Log.Warning("Failed to load font data for '" + Name + "': " + ex);
- // Loading failed (for Font to know), report new error for every font
- FailedToLoad = true;
- } // catch
- }
- #endregion
-
- #region ParseText
- /// <summary>
- /// Parses the given text by analyzing every character to decide if the
- /// current is supported or not. Every character that is not drawable will
- /// be replaced by the "fallback character" (or skipped if the font doesn't
- /// support even that). Addtionally by checking the characters also (every
- /// tyoe of) line breaks will be detected and teh text splitted by them.
- /// </summary>
- /// <param name="text">Text</param>
- /// <remarks>
- /// This method has no validation checks because every caller makes already
- /// sure that the text is valid.
- /// </remarks>
- internal List<List<char>> ParseText(string text)
- {
- // Our text lines we will return after parsing
- List<List<char>> finalTextlines = new List<List<char>>();
-
- // The parsed characters of the current text line
- List<char> textline = new List<char>();
-
- char[] textChars = text.ToCharArray();
- for (int charIndex = 0; charIndex < textChars.Length; charIndex++)
- {
- // Grab the current character we have to check now
- char textChar = textChars[charIndex];
-
- // Also compute the index of the next char which is used to "detect"
- // a Windows line break and when the text has ended
- int nextCharIndex = charIndex + 1;
- // and indicate if we have find a line break
- bool isLineBreak = false;
-
- #region Newlines
- // At first check for new-lines, possible values are:
- // - Windows = \r\n
- // - Unix = \n
- // - Macintosh = \r
-
- // Unix
- if (textChar == '\n')
- {
- isLineBreak = true;
- } // if
-
- // Macintosh or the first part of the Windows new-line
- else if (textChar == '\r')
- {
- // In both cases it's we need to go to the next line
- isLineBreak = true;
-
- // but also need to check for the second part of the Windows new-line
- if (nextCharIndex < textChars.Length &&
- textChars[nextCharIndex] == '\n')
- {
- // that we can ignore for the next loop-iteration
- charIndex++;
- } // if
- } // else if
- #endregion
-
- #region Tabs
- // Just convert a tab into 2 spaces
- else if (textChar == '\t')
- {
- for (int num = 0; num < 2; num++)
- {
- textline.Add(' ');
- } // for
- } // else if
- #endregion
-
- #region All normal characters (including space)
- // Ignore all other special characters (EOT, BackSpace, etc.)
- // and the newline that we maybe have handled above already
- else if (textChar >= ' ')
- {
- // Check now if the current text character is supported by the font
- // so we can allow it
- if (glyphDictionary.ContainsKey(textChar))
- {
- textline.Add(textChar);
- } // if
-
- // in the case it isn't supported then log a warning and use the
- // "fallback" character to indicate the character in the text which
- // is wrong
- else if (glyphDictionary.ContainsKey(FallbackChar))
- {
- Log.Warning("Sorry the current font '" + this + "' doesn't " +
- "support the character '" + textChar + "', so will use the '" +
- FallbackChar + "' as fallback character instead.");
- textline.Add(FallbackChar);
- } // else if
-
- // if even the "fallback" character isn't supported by the font then
- // log too but we have to skip this character then
- else
- {
- Log.Warning("Oops, can't even use a '?' in the text to indicate" +
- " that the character '" + textChar + "' is not available in" +
- " the font '" + this + "', the character will be skipped now.");
- continue;
- } // else
- } // else if
- #endregion
-
- // If we have detected a line break now
- if (isLineBreak ||
- // or have reached the last character of the text
- nextCharIndex == textChars.Length)
- {
- // then the parsing of the current text line is complete and we can
- // go to the next one
- finalTextlines.Add(textline);
- textline = new List<char>();
- } // if
- } // for
-
- return finalTextlines;
- }
- #endregion
-
- #region GetTextLines
- /// <summary>
- /// Gets the single text lines of multi line text by determining the line
- /// breaks. Each of the detected text line will be represented as a list of
- /// chars whereby only "known" characters will be listed here or at least
- /// represented by a question mark. All other characters will skipped and
- /// logged out.
- /// </summary>
- /// <remarks>
- /// This method will also handle clipping and word-wrapping.
- /// </remarks>
- /// <param name="text">Text</param>
- /// <param name="lineSpacing">Line spacing</param>
- /// <param name="maxTextSize">Maximum text size</param>
- /// <param name="textAlignment">Text alignment</param>
- /// <param name="isClippingOn">Is clipping on</param>
- /// <param name="isWordWrapOn">Is word wrap on</param>
- /// <param name="textlineWidths">Textline widths</param>
- /// <param name="maxTextlineWidth">Maximum textline width</param>
- /// <returns>
- /// The list of final text lines where the words contains only allowed
- /// characters.
- /// </returns>
- internal List<List<char>> GetTextLines(string text, float lineSpacing,
- Size maxTextSize, HorizontalAlignment textAlignment, bool isClippingOn,
- bool isWordWrapOn,
- out List<float> textlineWidths, out float maxTextlineWidth)
- {
- // At first initialize the 'out' values
- textlineWidths = new List<float>();
- maxTextlineWidth = 0.0f;
-
- // Next the list which contains the final text lines with characters for
- // each.
- List<List<char>> finalTextlines = new List<List<char>>();
-
- // Without a valid text we immediately stop here
- if (String.IsNullOrEmpty(text))
- {
- return finalTextlines;
- } // if
-
- // Compute now the total height which the font would need for every text
- // line
- float maxLineHeight = PixelLineHeight * lineSpacing;
-
- // In the case text clipping is enabled we check now if the available the
- // height of given text area is smaller than a character of the font
- // because then no text line will fit into it and we can also stop here
- // directly
- if (isClippingOn &&
- maxTextSize.Height < maxLineHeight)
- {
- return finalTextlines;
- } // if
-
- // Parse the given text now and get the text lines of it with all
- // supported characters
- List<List<char>> textlines = ParseText(text);
- // and iterate them all now
- for (int lineIndex = 0; lineIndex < textlines.Count; lineIndex++)
- {
- List<char> textlineChars = textlines[lineIndex];
-
- // Initialize here the list that will contain all characters of the
- // current text line
- List<char> textline = new List<char>();
- // and width (in pixels) of it which needs to start at 0
- float textlineWidth = 0.0f;
-
- // For the word-wrap feature we also need a container which collects
- // the characters of the current word we are building
- List<char> word = new List<char>();
- // and its width (in pixels)
- float wordWidth = 0.0f;
-
- // Last we still need the number of words we have already parsed to
- // "detect" if a single word exceeds the max. allowed text width and we
- // need to put it into the next text line separately
- int wordNumber = 0;
-
- for (int charId = 0; charId < textlineChars.Count; charId++)
- {
- // Get the current char of the current text line
- char textChar = textlineChars[charId];
-
- // and also figure out which is the next character which we need for
- // kerning, bearing and word- or line-ending detection
- int nextCharId = charId + 1;
- char nextChar = (nextCharId < textlineChars.Count)
- ? textlineChars[nextCharId]
- : NoChar;
-
- // Get now the width of the character which it will need to draw it
- // Note:
- // There is no need to check if a glyph exists because the method
- // "ParseText()" makes already sure that our text lines here contain
- // only supported characters (unsupported are converted in '?' or
- // skipped completely)
- Glyph glyph = glyphDictionary[textChar];
- int glyphWidth = glyph.GetDrawWidth(nextChar, Tracking);
-
- // and look if it's the first one of the current text line
- bool isFirstChar = charId == 0;
- // or the last one
- bool isLastChar = nextCharId == textlineChars.Count;
-
- // Unlike GetGlyphDrawInfos above we do not care about positioning,
- // just the width needed for this line (bearing reduces width).
- // But only take care about the bearing if we have here still a
- // character at the right of us
- if (nextChar != NoChar)
- {
- if (isFirstChar &&
- textAlignment == HorizontalAlignment.Left)
- {
- glyphWidth -= MathHelper.Round(
- glyphDictionary[nextChar].LeftSideBearing);
- } // if
-
- if (isLastChar &&
- textAlignment == HorizontalAlignment.Right)
- {
- glyphWidth += MathHelper.Round(
- glyphDictionary[nextChar].RightSideBearing);
- } // if
- } // if
-
- #region Word-Wrap based clipping
- // The word-wrap feature makes only sense with clipping otherwise
- // there would be no need to wrap text lines
- if (isClippingOn &&
- isWordWrapOn)
- {
- bool isSpace = textChar == ' ';
- // "Flag" to know when a word ist fully parsed
- bool isWordFinished = false;
-
- #region Word separating by a white space
- if (isSpace)
- {
- // Currently add the space character always at the end of the
- // word independend by the set text alignment
- // Decide if and where to add the space depending on the current
- // set text alignment
- word.Add(textChar);
- // We will add the width of the space later after checking if the
- // word has still fit in the currently available text line space
- // because it's only a whitespace and doesn't belongs to the word
- //wordLength += glyphWidth;
- isWordFinished = true;
- } // if
- #endregion
-
- #region Last word of the current line has ended
- else if (isLastChar)
- {
- word.Add(textChar);
- wordWidth += glyphWidth;
- isWordFinished = true;
- } // else if
- #endregion
-
- #region Building the current word
- else
- {
- word.Add(textChar);
- wordWidth += glyphWidth;
- } // else
- #endregion
-
- // If a word is fully parsed now
- if (isWordFinished)
- {
- // then check if the current text line has still enough space to
- // add it
- if (textlineWidth + wordWidth <= maxTextSize.Width ||
- // and ignore clipping if even the first word of the current
- // text line wouldn't fit into it (to avoid that the word would
- // "push" the rest of the text down and out of the text area
- wordNumber == 0)
- {
- // Update the parsed result of the current text line now
- textline.AddRange(word);
- textlineWidth += wordWidth;
- // and also add the width of the space now that we have skipped
- // above
- if (isSpace)
- {
- textlineWidth += glyphWidth;
- } // if
-
- wordNumber++;
- } // if
-
- // In the case the next word would not fit anymore
- else
- {
- // we also grab the remaining text line part
- while (nextCharId < textlineChars.Count)
- {
- word.Add(textlineChars[nextCharId]);
- nextCharId++;
- } // while
- // and insert it as next text line (-> wrapping)
- textlines.Insert(lineIndex + 1, word);
-
- // Last indicate to the character loop of the current text line
- // that we have "jumped" to the end
- charId = textlineChars.Count;
- } // else
-
- // before setting the initial values again for the word building
- // so that parsing of the next text line can be started now
- word = new List<char>();
- wordWidth = 0.0f;
- } // if
- } // if
- #endregion
-
- #region Character based clipping and non-clipping
- // If no clipping is required
- else if (isClippingOn == false ||
- // or in the case that (horizontal) clipping is enabled and we
- // check if the current character would still fit in the remaining
- // space of the text line
- // (-> vertical clipping will be handled more below)
- (textlineWidth + glyphWidth) <= maxTextSize.Width)
- {
- // Add the character to the current text line characters
- textline.Add(textChar);
- // increase the width of current text line by it
- textlineWidth += glyphWidth;
- } // else if
-
- // if the available space limit of the current text line is reached
- else
- {
- // we can abort the parsing of this line and go to the next one
- break;
- } // else
- #endregion
- } // for
-
- // Every time a text line is parsed add it as final one
- finalTextlines.Add(textline);
- // inclusive the measured width of it
- textlineWidths.Add(textlineWidth);
-
- // and additionally check if the current text line was the longest
- // that we had so far
- if (maxTextlineWidth < textlineWidth)
- {
- maxTextlineWidth = textlineWidth;
- } // if
-
- #region Vertical text clipping check
- // Now check if there is still enough space for the next text line if
- // (vertical) clipping is wished
- if (isClippingOn)
- {
- float requiredHeightForNextLine =
- (finalTextlines.Count + 1) * maxLineHeight;
- // If no more text line would fit
- if (requiredHeightForNextLine > maxTextSize.Height)
- {
- // we can stop here completely and are done now with parsing of
- // the text lines
- break;
- } // if
-
- // Note:
- // Horizontal clipping was already handled above
- } // if
- #endregion
-
- // start the text line parsing at the beginning (again)
- textline = new List<char>();
- textlineWidth = 0.0f;
- } // for
-
- return finalTextlines;
- }
- #endregion
-
- #endregion
-
- /// <summary>
- /// Tests
- /// </summary>
- internal class FontDataTests
- {
- #region Helpers
- /// <summary>
- /// Get font data which is used for all tests where the tests values are
- /// tweaked for.
- /// </summary>
- private static FontData GetTestFont()
- {
- // We use the Verdana 12 pt font
- return FontData.Get("Verdana", 12, FontStyle.AddOutline).
- ResolutionFonts[2];
- }
- #endregion
-
- #region LoadDefaultFontData (LongRunning)
- /// <summary>
- /// Load default font data
- /// </summary>
- [Test, Category("LongRunning")]
- public void LoadDefaultFontData()
- {
- FontData fontData = GetTestFont();
-
- Assert.NotNull(fontData.data);
- Assert.Equal(fontData.glyphDictionary.Count, 95);
- Assert.Equal(fontData.FontMapNames.Length, 1);
- Assert.Equal(fontData.FontMapNames[0], "Verdana_12_16");
- }
- #endregion
-
- #region ParseTextLines (LongRunning)
- /// <summary>
- /// Parse text lines
- /// </summary>
- [Test, Category("LongRunning")]
- public void ParseTextLines()
- {
- FontData fontData = GetTestFont();
-
- List<List<char>> lines = fontData.ParseText(
- "long loong looong loooong text");
- Assert.NotNull(lines);
- Assert.Equal(lines.Count, 1);
- Assert.Equal(lines[0].ToText(), "long loong looong loooong text");
-
- lines = fontData.ParseText(
- "long loong looong loooong text" + Environment.NewLine +
- "Newline");
- Assert.NotNull(lines);
- Assert.Equal(lines.Count, 2);
- Assert.Equal(lines[0].ToText(), "long loong looong loooong text");
- Assert.Equal(lines[1].ToText(), "Newline");
- }
- #endregion
-
- #region GetTextLines (LongRunning)
- /// <summary>
- /// Get text lines
- /// </summary>
- [Test, Category("LongRunning")]
- public void GetTextLines()
- {
- string multilineText =
- "This" + "\n" +
- "is a" + "\r\n" +
- "multiline" + "\r" +
- "text.";
-
- FontData fontData = GetTestFont();
-
- List<float> textlineWidths;
- float maxTextlineWidth;
- List<List<char>> textlines = fontData.GetTextLines(multilineText,
- 1.0f, Size.Zero, HorizontalAlignment.Left, false, false,
- out textlineWidths, out maxTextlineWidth);
-
- Assert.Equal(textlineWidths.Count, 4);
- Assert.Equal(textlines[0].ToText(), "This");
- Assert.Equal(textlines[1].ToText(), "is a");
- Assert.Equal(textlines[2].ToText(), "multiline");
- Assert.Equal(textlines[3].ToText(), "text.");
-
- Assert.Equal(textlines.Count, textlineWidths.Count);
- Dictionary<char, Glyph> glyphs = fontData.glyphDictionary;
- // (8-0) + 8 + 3 + 6 = 25
- //Assert.Equal(textlineWidths[0],
- Assert.Equal(textlineWidths[0] + 1,
- MathHelper.Round(glyphs['T'].AdvanceWidth -
- glyphs['T'].LeftSideBearing) +
- MathHelper.Round(glyphs['h'].AdvanceWidth) +
- MathHelper.Round(glyphs['i'].AdvanceWidth) +
- MathHelper.Round(glyphs['s'].AdvanceWidth));
- Assert.Equal(textlineWidths[0], 24);
-
- // (3-1) + 6 + 4 + 7 = 19
- Assert.Equal(textlineWidths[1],
- MathHelper.Round(glyphs['i'].AdvanceWidth -
- glyphs['i'].LeftSideBearing) +
- MathHelper.Round(glyphs['s'].AdvanceWidth) +
- MathHelper.Round(glyphs[' '].AdvanceWidth) +
- MathHelper.Round(glyphs['a'].AdvanceWidth));
- Assert.Equal(textlineWidths[1], 19);
-
- // (12-1) + 8 + 3 + 5 + 3 + 3 + 3 + 8 + 7 = 51
- Assert.Equal(textlineWidths[2],
- MathHelper.Round(glyphs['m'].AdvanceWidth -
- glyphs['m'].LeftSideBearing) +
- MathHelper.Round(glyphs['u'].AdvanceWidth) +
- MathHelper.Round(glyphs['l'].AdvanceWidth) +
- MathHelper.Round(glyphs['t'].AdvanceWidth) +
- MathHelper.Round(glyphs['i'].AdvanceWidth) +
- MathHelper.Round(glyphs['l'].AdvanceWidth) +
- MathHelper.Round(glyphs['i'].AdvanceWidth) +
- MathHelper.Round(glyphs['n'].AdvanceWidth) +
- MathHelper.Round(glyphs['e'].AdvanceWidth));
- Assert.Equal(textlineWidths[2], 51);
-
- // (4 - 0) + 7 + 7 + 5 + 4 = 27
- Assert.Equal(textlineWidths[3],
- MathHelper.Round(glyphs['t'].AdvanceWidth -
- glyphs['t'].LeftSideBearing) +
- MathHelper.Round(glyphs['e'].AdvanceWidth) +
- MathHelper.Round(glyphs['x'].AdvanceWidth) +
- MathHelper.Round(glyphs['t'].AdvanceWidth) +
- MathHelper.Round(glyphs['.'].AdvanceWidth));
- Assert.Equal(textlineWidths[3], 27);
-
- Assert.Equal(maxTextlineWidth, 51);
- }
- #endregion
-
- #region GetTextlineWrapped (LongRunning)
- /// <summary>
- /// Get text line wrapped
- /// </summary>
- /// Caution: For this unit test are the following modules required:
- /// - Graphic
- /// - Platforms.IWindow
- /// - ContentManager.Client
- [Test, Category("LongRunning")]
- public void GetTextlineWrapped()
- {
- string text =
- "long loong looong loooong text" + Environment.NewLine + "Newline";
-
- FontData fontData = GetTestFont();
- Size textAreaSize = new Size(138, fontData.PixelLineHeight * 3);
-
- List<float> textlineWidths;
- float maxTextlineWidth;
- List<List<char>> lines = fontData.GetTextLines(text, 1.0f,
- textAreaSize, HorizontalAlignment.Left, true, true,
- out textlineWidths, out maxTextlineWidth);
-
- Assert.NotNull(lines);
- Assert.Equal(lines.Count, 3);
- Assert.Equal(lines[0].ToText(), "long loong looong ");
- Assert.Equal(lines[1].ToText(), "loooong text");
- Assert.Equal(lines[2].ToText(), "Newline");
- }
- #endregion
-
- #region GetGlyphDrawInfos
- /// <summary>
- /// Get glyph draw infos
- /// </summary>
- [Test, Category("LongRunning")]
- public void GetGlyphDrawInfos()
- {
- FontData testFont = FontData.Default;
-
- GlyphDrawInfo[] drawGlyphs = testFont.GetGlyphDrawInfos("", 1.0f,
- HorizontalAlignment.Left);
- Assert.Equal(0, drawGlyphs.Length);
-
- drawGlyphs = testFont.GetGlyphDrawInfos("\n", 1.0f,
- HorizontalAlignment.Left);
- Assert.Equal(drawGlyphs.Length, 1);
- Rectangle newLineArea = drawGlyphs[0].DrawArea;
- Assert.Equal(newLineArea.X, 0);
- Assert.Equal(newLineArea.Y, testFont.PixelLineHeight);
- Assert.Equal(newLineArea.Width, 0);
- Assert.Equal(newLineArea.Height, testFont.PixelLineHeight);
-
- drawGlyphs = testFont.GetGlyphDrawInfos("A", 1.0f,
- HorizontalAlignment.Left);
- Assert.Equal(drawGlyphs.Length, 1);
- GlyphDrawInfo glyphA = drawGlyphs[0];
- Assert.Equal(glyphA.UV, Rectangle.BuildUVRectangle(
- new Rectangle(67, 32, 9, 16), new Size(128)));
- Assert.Equal(glyphA.FontMapId, 0);
- }
- #endregion
- }
- }
- }