using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
using Svg.DataTypes;
using System.Linq;
namespace Svg
{
public enum XmlSpaceHandling
{
@default,
preserve
}
public abstract class SvgTextBase : SvgVisualElement
{
private SvgUnit _x;
private SvgUnit _y;
private SvgUnit _dy;
private SvgUnit _dx;
private SvgUnit _letterSpacing;
private SvgUnit _wordSpacing;
private SvgUnit _fontSize;
private SvgFontWeight _fontWeight;
private string _font;
protected string _fontFamily;
private SvgTextAnchor _textAnchor = SvgTextAnchor.Start;
private static readonly SvgRenderer _stringMeasure;
private const string DefaultFontFamily = "Times New Roman";
private XmlSpaceHandling _space = XmlSpaceHandling.@default;
///
/// Initializes the class.
///
static SvgTextBase()
{
Bitmap bitmap = new Bitmap(1, 1);
_stringMeasure = SvgRenderer.FromImage(bitmap);
_stringMeasure.TextRenderingHint = TextRenderingHint.AntiAlias;
}
///
/// Initializes a new instance of the class.
///
public SvgTextBase()
{
this._fontSize = new SvgUnit(0.0f);
this._dy = new SvgUnit(0.0f);
this._dx = new SvgUnit(0.0f);
}
///
/// Gets or sets the text to be rendered.
///
public virtual string Text
{
get { return base.Content; }
set { base.Content = value; this.IsPathDirty = true; this.Content = value; }
}
///
/// Gets or sets the text anchor.
///
/// The text anchor.
[SvgAttribute("text-anchor")]
public virtual SvgTextAnchor TextAnchor
{
get { return this._textAnchor; }
set { this._textAnchor = value; this.IsPathDirty = true; }
}
///
/// Gets or sets the X.
///
/// The X.
[SvgAttribute("x")]
public virtual SvgUnit X
{
get { return this._x; }
set
{
if(_x != value)
{
this._x = value;
this.IsPathDirty = true;
OnAttributeChanged(new AttributeEventArgs{ Attribute = "x", Value = value });
}
}
}
///
/// Gets or sets the dX.
///
/// The dX.
[SvgAttribute("dx")]
public virtual SvgUnit Dx
{
get { return this._dx; }
set
{
if (_dx != value)
{
this._dx = value;
this.IsPathDirty = true;
OnAttributeChanged(new AttributeEventArgs { Attribute = "dx", Value = value });
}
}
}
///
/// Gets or sets the Y.
///
/// The Y.
[SvgAttribute("y")]
public virtual SvgUnit Y
{
get { return this._y; }
set
{
if(_y != value)
{
this._y = value;
this.IsPathDirty = true;
OnAttributeChanged(new AttributeEventArgs{ Attribute = "y", Value = value });
}
}
}
///
/// Gets or sets the dY.
///
/// The dY.
[SvgAttribute("dy")]
public virtual SvgUnit Dy
{
get { return this._dy; }
set
{
if (_dy != value)
{
this._dy = value;
this.IsPathDirty = true;
OnAttributeChanged(new AttributeEventArgs { Attribute = "dy", Value = value });
}
}
}
///
/// Specifies spacing behavior between text characters.
///
[SvgAttribute("letter-spacing")]
public virtual SvgUnit LetterSpacing
{
get { return this._letterSpacing; }
set { this._letterSpacing = value; this.IsPathDirty = true; }
}
///
/// Specifies spacing behavior between words.
///
[SvgAttribute("word-spacing")]
public virtual SvgUnit WordSpacing
{
get { return this._wordSpacing; }
set { this._wordSpacing = value; this.IsPathDirty = true; }
}
///
/// Indicates which font family is to be used to render the text.
///
[SvgAttribute("font-family")]
public virtual string FontFamily
{
get { return this._fontFamily ?? DefaultFontFamily; }
set
{
this._fontFamily = ValidateFontFamily(value);
this.IsPathDirty = true;
}
}
///
/// Refers to the size of the font from baseline to baseline when multiple lines of text are set solid in a multiline layout environment.
///
[SvgAttribute("font-size")]
public virtual SvgUnit FontSize
{
get { return this._fontSize; }
set { this._fontSize = value; this.IsPathDirty = true; }
}
///
/// Refers to the boldness of the font.
///
[SvgAttribute("font-weight")]
public virtual SvgFontWeight FontWeight
{
get { return this._fontWeight; }
set { this._fontWeight = value; this.IsPathDirty = true; }
}
///
/// Set all font information.
///
[SvgAttribute("font")]
public virtual string Font
{
get { return this._font; }
set
{
var parts = value.Split(',');
foreach (var part in parts)
{
//This deals with setting font size. Looks for either px or pt style="font: bold 16px/normal 'trebuchet ms', verdana, sans-serif;"
Regex rx = new Regex(@"(\d+)+(?=pt|px)");
var res = rx.Match(part);
if (res.Success)
{
int fontSize = 10;
int.TryParse(res.Value, out fontSize);
this.FontSize = new SvgUnit((float)fontSize);
}
//this assumes "bold" has spaces around it. e.g.: style="font: bold 16px/normal
rx = new Regex(@"\sbold\s");
res = rx.Match(part);
if (res.Success)
{
this.FontWeight = SvgFontWeight.bold;
}
}
var font = ValidateFontFamily(value);
this._fontFamily = font;
this._font = font; //not sure this is used?
this.IsPathDirty = true;
}
}
///
/// Gets or sets the fill.
///
///
/// Unlike other s, has a default fill of black rather than transparent.
///
/// The fill.
public override SvgPaintServer Fill
{
get { return (this.Attributes["fill"] == null) ? new SvgColourServer(Color.Black) : (SvgPaintServer)this.Attributes["fill"]; }
set { this.Attributes["fill"] = value; }
}
///
/// Returns a that represents the current .
///
///
/// A that represents the current .
///
public override string ToString()
{
return this.Text;
}
///
/// Gets or sets a value to determine if anti-aliasing should occur when the element is being rendered.
///
///
protected override bool RequiresSmoothRendering
{
get { return true; }
}
///
/// Gets the bounds of the element.
///
/// The bounds.
public override System.Drawing.RectangleF Bounds
{
get { return this.Path.GetBounds(); }
}
static private RectangleF MeasureString(SvgRenderer renderer, string text, Font font)
{
GraphicsPath p = new GraphicsPath();
p.AddString(text, font.FontFamily, 0, font.Size, new PointF(0.0f, 0.0f), StringFormat.GenericTypographic);
p.Transform(renderer.Transform);
return p.GetBounds();
}
private static string ValidateFontFamily(string fontFamilyList)
{
// Split font family list on "," and then trim start and end spaces and quotes.
var fontParts = fontFamilyList.Split(new[] { ',' }).Select(fontName => fontName.Trim(new[] { '"', ' ','\'' }));
var families = System.Drawing.FontFamily.Families;
// Find a the first font that exists in the list of installed font families.
//styles from IE get sent through as lowercase.
foreach (var f in fontParts.Where(f => families.Any(family => family.Name.ToLower() == f.ToLower())))
{
return f;
}
// No valid font family found from the list requested.
return null;
}
///
/// Renders the and contents to the specified object.
///
/// The object to render to.
/// Necessary to make sure that any internal tspan elements get rendered as well
protected override void Render(SvgRenderer renderer)
{
if ((this.Path != null) && this.Visible && this.Displayable)
{
this.PushTransforms(renderer);
this.SetClip(renderer);
// If this element needs smoothing enabled turn anti-aliasing on
if (this.RequiresSmoothRendering)
{
renderer.SmoothingMode = SmoothingMode.AntiAlias;
}
this.RenderFill(renderer);
this.RenderStroke(renderer);
this.RenderChildren(renderer);
// Reset the smoothing mode
if (this.RequiresSmoothRendering && renderer.SmoothingMode == SmoothingMode.AntiAlias)
{
renderer.SmoothingMode = SmoothingMode.Default;
}
this.ResetClip(renderer);
this.PopTransforms(renderer);
}
}
private GraphicsPath _path;
protected class NodeBounds
{
public float xOffset { get; set; }
public SizeF Bounds { get; set; }
public ISvgNode Node { get; set; }
}
protected class BoundsData
{
private List _nodes = new List();
public IList Nodes
{
get { return _nodes; }
}
public SizeF Bounds { get; set; }
}
protected BoundsData GetTextBounds()
{
var font = GetFont();
SvgTextBase innerText;
SizeF stringBounds;
float totalHeight = 0;
float totalWidth = 0;
var result = new BoundsData();
var nodes = (from n in this.Nodes
where (n is SvgContentNode || n is SvgTextBase) && !string.IsNullOrEmpty(n.Content)
select n).ToList();
ISvgNode node;
for (var i = 0; i < nodes.Count; i++)
{
node = nodes[i];
lock (_stringMeasure)
{
innerText = node as SvgTextBase;
if (innerText == null)
{
stringBounds = _stringMeasure.MeasureString(PrepareText(node.Content,
i > 0 && nodes[i - 1] is SvgTextBase,
i < nodes.Count - 1 && nodes[i + 1] is SvgTextBase), font);
}
else
{
stringBounds = innerText.GetTextBounds().Bounds;
}
result.Nodes.Add(new NodeBounds() { Bounds = stringBounds, Node = node, xOffset = totalWidth });
totalHeight = Math.Max(totalHeight, stringBounds.Height);
totalWidth += stringBounds.Width;
}
}
result.Bounds = new SizeF(totalWidth, totalHeight);
return result;
}
protected float _calcX = 0;
protected float _calcY = 0;
///
/// Gets the for this element.
///
///
public override System.Drawing.Drawing2D.GraphicsPath Path
{
get
{
// Make sure the path is always null if there is no text
//if there is a TSpan inside of this text element then path should not be null (even if this text is empty!)
if ((string.IsNullOrEmpty(this.Text) || this.Text.Trim().Length < 1) && this.Children.Where(x => x is SvgTextSpan).Select(x => x as SvgTextSpan).Count() == 0)
return _path = null;
//NOT SURE WHAT THIS IS ABOUT - Path gets created again anyway - WTF?
// When an empty string is passed to GraphicsPath, it rises an InvalidArgumentException...
if (_path == null || this.IsPathDirty)
{
var font = GetFont();
SvgTextBase innerText;
//RectangleF bounds;
float x = (_x == SvgUnit.Empty || _x == SvgUnit.None ? _calcX : _x.ToDeviceValue(this)) + _dx.ToDeviceValue(this);
float y = (_y == SvgUnit.Empty || _y == SvgUnit.None ? _calcY : _y.ToDeviceValue(this, true)) + _dy.ToDeviceValue(this, true);
_path = new GraphicsPath();
_path.StartFigure();
// Measure the overall bounds of all the text
var boundsData = GetTextBounds();
// Determine the location of the start point
switch (this.TextAnchor)
{
case SvgTextAnchor.Middle:
x -= (boundsData.Bounds.Width / 2);
break;
case SvgTextAnchor.End:
x -= boundsData.Bounds.Width;
break;
}
NodeBounds data;
for (var i = 0; i < boundsData.Nodes.Count; i++)
{
data = boundsData.Nodes[i];
innerText = data.Node as SvgTextBase;
if (innerText == null)
{
// Minus FontSize because the x/y coords mark the bottom left, not bottom top.
DrawString(_path, x + data.xOffset, y - boundsData.Bounds.Height, font,
PrepareText(data.Node.Content, i > 0 && boundsData.Nodes[i-1].Node is SvgTextBase,
i < boundsData.Nodes.Count - 1 && boundsData.Nodes[i + 1].Node is SvgTextBase));
}
else
{
innerText._calcX = x + data.xOffset;
innerText._calcY = y;
}
}
_path.CloseFigure();
this.IsPathDirty = false;
}
return _path;
}
protected set
{
_path = value;
}
}
///
/// Prepare the text according to the whitespace handling rules. SVG Spec.
///
/// Text to be prepared
/// Prepared text
protected string PrepareText(string value, bool leadingSpace, bool trailingSpace)
{
if (_space == XmlSpaceHandling.preserve)
{
return (leadingSpace ? " " : "") + value.Replace('\t', ' ').Replace("\r\n", " ").Replace('\r', ' ').Replace('\n', ' ') + (trailingSpace ? " " : "");
}
else
{
return (leadingSpace ? " " : "") + value.Replace("\r", "").Replace("\n", "").Replace('\t', ' ').Trim().Replace(" ", " ") + (trailingSpace ? " " : "");
}
}
///
/// Get the font information based on data stored with the text object or inherited from the parent.
///
///
internal Font GetFont()
{
var parent = this.Parent as SvgTextBase;
Font parentFont = null;
if (parent != null) parentFont = parent.GetFont();
float fontSize = this.FontSize.ToDeviceValue(this);
if (fontSize == 0.0f)
{
fontSize = (parentFont == null ? 1.0f : parentFont.Size);
fontSize = (fontSize == 0.0f ? 1.0f : fontSize);
}
var fontWeight = ((_fontWeight == SvgFontWeight.inherit && parentFont != null && parentFont.Bold) || _fontWeight == SvgFontWeight.bold ?
FontStyle.Bold : FontStyle.Regular);
var family = _fontFamily ?? (parentFont == null ? DefaultFontFamily : parentFont.FontFamily.Name);
return new Font(family, fontSize, fontWeight, GraphicsUnit.Pixel);
}
///
/// Draws a string on a path at a specified location and with a specified font.
///
internal void DrawString(GraphicsPath path, float x, float y, Font font, string text)
{
PointF location = new PointF(x, y);
// No way to do letter-spacing or word-spacing, so do manually
if (this.LetterSpacing.Value > 0.0f || this.WordSpacing.Value > 0.0f)
{
// Cut up into words, or just leave as required
string[] words = (this.WordSpacing.Value > 0.0f) ? text.Split(' ') : new string[] { text };
float wordSpacing = this.WordSpacing.ToDeviceValue(this);
float letterSpacing = this.LetterSpacing.ToDeviceValue(this);
float start = this.X.ToDeviceValue(this);
foreach (string word in words)
{
// Only do if there is line spacing, just write the word otherwise
if (this.LetterSpacing.Value > 0.0f)
{
char[] characters = word.ToCharArray();
foreach (char currentCharacter in characters)
{
path.AddString(currentCharacter.ToString(), font.FontFamily, (int)font.Style, font.Size, location, StringFormat.GenericTypographic);
location = new PointF(path.GetBounds().Width + start + letterSpacing, location.Y);
}
}
else
{
path.AddString(word, font.FontFamily, (int)font.Style, font.Size, location, StringFormat.GenericTypographic);
}
// Move the location of the word to be written along
location = new PointF(path.GetBounds().Width + start + wordSpacing, location.Y);
}
}
else
{
if (!string.IsNullOrEmpty(text))
{
path.AddString(text, font.FontFamily, (int)font.Style, font.Size, location, StringFormat.GenericTypographic);
}
}
}
[SvgAttribute("onchange")]
public event EventHandler Change;
//change
protected void OnChange(string newString, string sessionID)
{
RaiseChange(this, new StringArg {s = newString, SessionID = sessionID});
}
protected void RaiseChange(object sender, StringArg s)
{
var handler = Change;
if (handler != null)
{
handler(sender, s);
}
}
#if Net4
public override void RegisterEvents(ISvgEventCaller caller)
{
//register basic events
base.RegisterEvents(caller);
//add change event for text
caller.RegisterAction(this.ID + "/onchange", OnChange);
}
public override void UnregisterEvents(ISvgEventCaller caller)
{
//unregister base events
base.UnregisterEvents(caller);
//unregister change event
caller.UnregisterAction(this.ID + "/onchange");
}
#endif
}
}