#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
<int, TextExtents
> block_cache
= new Dictionary
<int, TextExtents
>();
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
<RectangleF
> measured_glyphs
= new List
<RectangleF
>(256);
readonly ObjectPool
<PoolableTextExtents
> text_extents_pool
= new ObjectPool
<PoolableTextExtents
>();
// 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
<RectangleF
> 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
}
}