#region License // // The Open Toolkit Library License // // Copyright (c) 2006 - 2008 the Open Toolkit library, except where noted. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.Drawing.Text; using OpenTK.Platform; namespace OpenTK.Graphics.Text { sealed class GdiPlusGlyphRasterizer : IGlyphRasterizer { #region Fields // Note: as an optimization, we store the TextBlock hashcode instead of the TextBlock itself. Dictionary block_cache = new Dictionary(); System.Drawing.Graphics graphics = System.Drawing.Graphics.FromImage(new Bitmap(1, 1)); IntPtr[] regions = new IntPtr[GdiPlus.MaxMeasurableCharacterRanges]; CharacterRange[] characterRanges = new CharacterRange[GdiPlus.MaxMeasurableCharacterRanges]; Bitmap glyph_surface; System.Drawing.Graphics glyph_renderer; readonly List measured_glyphs = new List(256); readonly ObjectPool text_extents_pool = new ObjectPool(); // Check the constructor, too, for additional flags. // Used for measuring text. Can set the leftToRight, rightToLeft, vertical and measure trailing spaces flags. readonly StringFormat measure_string_format = new StringFormat(StringFormat.GenericDefault); readonly StringFormat measure_string_format_tight = new StringFormat(StringFormat.GenericTypographic); // Used for loading glyphs. Only use leftToRight! readonly StringFormat load_glyph_string_format = new StringFormat(StringFormat.GenericDefault); readonly StringFormat load_glyph_string_format_tight = new StringFormat(StringFormat.GenericTypographic); static readonly char[] newline_characters = new char[] { '\n', '\r' }; static readonly SizeF MaximumGraphicsClipSize; #endregion #region Constructors static GdiPlusGlyphRasterizer() { using (Bitmap bmp = new Bitmap(1, 1)) using (System.Drawing.Graphics gfx = System.Drawing.Graphics.FromImage(bmp)) { MaximumGraphicsClipSize = gfx.ClipBounds.Size; } } public GdiPlusGlyphRasterizer() { measure_string_format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces | StringFormatFlags.NoClip; measure_string_format_tight.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; } #endregion #region IGlyphRasterizer Members #region Rasterize public Bitmap Rasterize(Glyph glyph) { return Rasterize(glyph, TextQuality.Default); } public Bitmap Rasterize(Glyph glyph, TextQuality quality) { EnsureSurfaceSize(ref glyph_surface, ref glyph_renderer, glyph.Font); SetTextRenderingOptions(glyph_renderer, glyph.Font, quality); RectangleF r2 = new RectangleF(); glyph_renderer.Clear(Color.Transparent); glyph_renderer.DrawString(glyph.Character.ToString(), glyph.Font, Brushes.White, Point.Empty, //new Point(glyph_surface.Width, 0), glyph.Font.Style == FontStyle.Italic ? load_glyph_string_format : load_glyph_string_format_tight); r2 = FindEdges(glyph_surface, true); //if ((default_string_format.FormatFlags & StringFormatFlags.DirectionRightToLeft) != 0) //{ // glyph_renderer.DrawString(glyph.Character.ToString(), glyph.Font, Brushes.White, Point.Empty, //new Point(glyph_surface.Width, 0), // load_glyph_string_format);//glyph.Font.Style == FontStyle.Italic ? load_glyph_string_format : default_string_format); // r2 = FindEdges(glyph_surface, true); //} //else //{ // glyph_renderer.DrawString(glyph.Character.ToString(), glyph.Font, Brushes.White, Point.Empty, // load_glyph_string_format_tight); //glyph.Font.Style == FontStyle.Italic ? load_glyph_string_format : default_string_format); // r2 = FindEdges(glyph_surface, false); //} return glyph_surface.Clone(r2, System.Drawing.Imaging.PixelFormat.Format32bppArgb); } #endregion #region MeasureText public TextExtents MeasureText(ref TextBlock block) { return MeasureText(ref block, TextQuality.Default); } public TextExtents MeasureText(ref TextBlock block, TextQuality quality) { // First, check if we have cached this text block. Do not use block_cache.TryGetValue, to avoid thrashing // the user's TextBlockExtents struct. int hashcode = block.GetHashCode(); if (block_cache.ContainsKey(hashcode)) return block_cache[hashcode]; // If this block is not cached, we have to measure it and (potentially) place it in the cache. TextExtents extents = MeasureTextExtents(ref block, quality); if ((block.Options & TextPrinterOptions.NoCache) == 0) block_cache.Add(hashcode, extents); return extents; } #endregion #region Clear public void Clear() { block_cache.Clear(); } #endregion #endregion #region Private Members #region EnsureSurfaceSize void EnsureSurfaceSize(ref Bitmap bmp, ref System.Drawing.Graphics gfx, Font font) { if (bmp == null || bmp.Width < 2 * font.Size || bmp.Height < 2 * font.Size) { if (bmp != null) bmp.Dispose(); if (gfx != null) gfx.Dispose(); bmp = new Bitmap((int)(2 * font.Size), (int)(2 * font.Size)); gfx = System.Drawing.Graphics.FromImage(bmp); } } #endregion #region SetRenderingOptions // Modify rendering settings (antialiasing, grid fitting) to improve appearance. void SetTextRenderingOptions(System.Drawing.Graphics gfx, Font font, TextQuality quality) { switch (quality) { case TextQuality.Default: gfx.TextRenderingHint = TextRenderingHint.SystemDefault; break; case TextQuality.High: gfx.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; break; case TextQuality.Medium: if (font.Size <= 18.0f) gfx.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; else gfx.TextRenderingHint = TextRenderingHint.AntiAlias; break; case TextQuality.Low: if (font.Size <= 18.0f) gfx.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; else gfx.TextRenderingHint = TextRenderingHint.SingleBitPerPixel; break; } } #endregion #region MeasureTextExtents TextExtents MeasureTextExtents(ref TextBlock block, TextQuality quality) { // Todo: Parse layout options: StringFormat format = block.Font.Italic ? measure_string_format : measure_string_format_tight; //StringFormat format = measure_string_format_tight; if (block.Direction == TextDirection.Vertical) format.FormatFlags |= StringFormatFlags.DirectionVertical; else format.FormatFlags &= ~StringFormatFlags.DirectionVertical; if (block.Direction == TextDirection.RightToLeft) format.FormatFlags |= StringFormatFlags.DirectionRightToLeft; else format.FormatFlags &= ~StringFormatFlags.DirectionRightToLeft; if (block.Alignment == TextAlignment.Near) format.Alignment = StringAlignment.Near; else if (block.Alignment == TextAlignment.Center) format.Alignment = StringAlignment.Center; else format.Alignment = StringAlignment.Far; TextExtents extents = text_extents_pool.Acquire(); RectangleF rect = block.Bounds; // Work around Mono/GDI+ bug, which causes incorrect // text wraping when block.Bounds == SizeF.Empty. if (block.Bounds.Size == SizeF.Empty) rect.Size = MaximumGraphicsClipSize; SetTextRenderingOptions(graphics, block.Font, quality); IntPtr native_graphics = GdiPlus.GetNativeGraphics(graphics); IntPtr native_font = GdiPlus.GetNativeFont(block.Font); IntPtr native_string_format = GdiPlus.GetNativeStringFormat(format); float max_width = 0, max_height = 0; // It seems that the mere presence of \n and \r characters // is enough for Mono to botch the layout (even if these // characters are not processed.) We'll need to find a // different way to perform layout on Mono, probably // through Pango. // Todo: This workaround allocates memory. //if (Configuration.RunningOnMono) { string[] lines = block.Text.Replace("\r", String.Empty).Split('\n'); foreach (string s in lines) { float width, height; extents.AddRange(MeasureGlyphExtents( ref block, s, native_graphics, native_font, native_string_format, ref rect, out width, out height)); if ((block.Direction & TextDirection.Vertical) == 0) rect.Y += block.Font.Height; else rect.X += block.Font.Height; if (width > max_width) max_width = width; if (height > max_height) max_height = height; } } if (extents.Count > 0) extents.BoundingBox = new RectangleF(extents[0].X, extents[0].Y, max_width, max_height); else extents.BoundingBox = RectangleF.Empty; return extents; } #endregion #region MeasureGlyphExtents // Gets the bounds of each character in a line of text. // Each line is processed in blocks of 32 characters (GdiPlus.MaxMeasurableCharacterRanges). IEnumerable MeasureGlyphExtents( ref TextBlock block, string text, IntPtr native_graphics, IntPtr native_font, IntPtr native_string_format, ref RectangleF layoutRect, out float max_width, out float max_height) { measured_glyphs.Clear(); max_width = layoutRect.Left; max_height = layoutRect.Top; float last_line_width = 0, last_line_height = 0; int current = 0; while (current < text.Length) { int num_characters = (text.Length - current) > GdiPlus.MaxMeasurableCharacterRanges ? GdiPlus.MaxMeasurableCharacterRanges : text.Length - current; int status = 0; // Prepare the character ranges and region structs for the measurement. for (int i = 0; i < num_characters; i++) { if (text[current + i] == '\n' || text[current + i] == '\r') throw new NotSupportedException(); characterRanges[i] = new CharacterRange(current + i, 1); IntPtr region; status = GdiPlus.CreateRegion(out region); regions[i] = region; Debug.Assert(status == 0, String.Format("GDI+ error: {0}", status)); } status = GdiPlus.SetStringFormatMeasurableCharacterRanges(native_string_format, num_characters, characterRanges); Debug.Assert(status == 0, String.Format("GDI+ error: {0}", status)); status = GdiPlus.MeasureCharacterRanges(native_graphics, text, text.Length, native_font, ref layoutRect, native_string_format, num_characters, regions); Debug.Assert(status == 0, String.Format("GDI+ error: {0}", status)); // Read back the results of the measurement. for (int i = 0; i < num_characters; i++) { RectangleF rect = new RectangleF(); GdiPlus.GetRegionBounds(regions[i], native_graphics, ref rect); Debug.Assert(status == 0, String.Format("GDI+ error: {0}", status)); GdiPlus.DeleteRegion(regions[i]); Debug.Assert(status == 0, String.Format("GDI+ error: {0}", status)); if (rect.Bottom > max_height) max_height = rect.Bottom; if (rect.Right > max_width) max_width = rect.Right; if (rect.X > last_line_width) last_line_width = rect.X; if (rect.Y > last_line_height) last_line_height = rect.Y; measured_glyphs.Add(rect); } current += num_characters; } // Make sure the current height is updated, if the the current line has wrapped due to word-wraping. // Otherwise, the next line will overlap with the current one. if (measured_glyphs.Count > 1) { if ((block.Direction & TextDirection.Vertical) == 0) { if (layoutRect.Y < last_line_height) layoutRect.Y = last_line_height; } else { if (layoutRect.X < last_line_width) layoutRect.X = last_line_width; } } // Mono's GDI+ implementation suffers from an issue where the specified layoutRect is not taken into // account. We will try to improve the situation by moving text to the correct location on this // error condition. This will not help word wrapping, but it is better than nothing. // Todo: Mono 2.8 is supposed to ship with a Pango-based GDI+ text renderer, which should not // suffer from this bug. Verify that this is the case and remove the hack. if (Configuration.RunningOnMono && (layoutRect.X != 0 || layoutRect.Y != 0) && measured_glyphs.Count > 0) { for (int i = 0; i < measured_glyphs.Count; i++) { RectangleF rect = measured_glyphs[i]; rect.X += layoutRect.X; rect.Y += layoutRect.Y; measured_glyphs[i] = rect; } } return measured_glyphs; } #endregion #region FindEdges #pragma warning disable 0649 struct Pixel { public byte B, G, R, A; } #pragma warning restore 0649 // Note: The bool parameter is not used at this point. // We might need it if we ever load true rightToLeft glyphs. Rectangle FindEdges(Bitmap bmp, bool rightToLeft) { BitmapData data = bmp.LockBits( new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); //Rectangle rect = rightToLeft ? // Rectangle.FromLTRB(FindLeftEdge(bmp, data.Scan0), 0, bmp.Width - 1, FindBottomEdge(bmp, data.Scan0)) : // Rectangle.FromLTRB(0, 0, FindRightEdge(bmp, data.Scan0), FindBottomEdge(bmp, data.Scan0)); Rectangle rect = Rectangle.FromLTRB(0, 0, FindRightEdge(bmp, data.Scan0), FindBottomEdge(bmp, data.Scan0)); //Rectangle.FromLTRB(FindLeftEdge(bmp, data.Scan0), 0, FindRightEdge(bmp, data.Scan0), FindBottomEdge(bmp, data.Scan0)); bmp.UnlockBits(data); return rect; } #endregion #region Find[Left|Right|Top|Bottom]Edge // Iterates through the bmp, and returns the first row or line that contains a non-transparent pixels. int FindLeftEdge(Bitmap bmp, IntPtr ptr) { for (int x = 0; x < bmp.Width; x++) for (int y = 0; y < bmp.Height; y++) unsafe { if (((Pixel*)(ptr) + y * bmp.Width + x)->A != 0) return x; } return bmp.Width - 1; } int FindRightEdge(Bitmap bmp, IntPtr ptr) { for (int x = bmp.Width - 1; x >= 0; x--) for (int y = 0; y < bmp.Height; y++) unsafe { if (((Pixel*)(ptr) + y * bmp.Width + x)->A != 0) return x + 1; } return 0; } int FindTopEdge(Bitmap bmp, IntPtr ptr) { for (int y = 0; y < bmp.Height; y++) for (int x = 0; x < bmp.Width; x++) unsafe { if (((Pixel*)(ptr) + y * bmp.Width + x)->A != 0) return y; } return bmp.Height - 1; } int FindBottomEdge(Bitmap bmp, IntPtr ptr) { for (int y = bmp.Height - 1; y >= 0; y--) for (int x = 0; x < bmp.Width; x++) unsafe { if (((Pixel*)(ptr) + y * bmp.Width + x)->A != 0) return y + 1; } return 0; } #endregion #endregion } }