Commit d5c659a5 authored by Eric Domke's avatar Eric Domke
Browse files

Refactoring while working through W3C tests

- Adding W3C test cases and a test fixture
- Fixed support for CSS stylesheets (particularly when class names are
referenced)
- Refactoring unit calculations so that percentages and fractions
calculate more accurately
- SvgImage:
- Support PreserveAspectRatio attribute
- Support for referencing svg images
- Refactored text rendering to use the AttributeCollection inheritance
scheme
- Initial attempt at 'ex' unit support
- Added support for system color names
- Changed parsing of entities to support XML entities
- Supporting loading of a svg document directly from a XmlDocument with
requiring serializing the document as a string first.
- ...
parent 3aedd8e8
...@@ -82,6 +82,7 @@ ...@@ -82,6 +82,7 @@
<AssemblyOriginatorKeyFile>svgkey.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>svgkey.snk</AssemblyOriginatorKeyFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="PresentationCore" />
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="System.Web" /> <Reference Include="System.Web" />
...@@ -99,6 +100,9 @@ ...@@ -99,6 +100,9 @@
<Compile Include="Clipping and Masking\SvgClipRule.cs" /> <Compile Include="Clipping and Masking\SvgClipRule.cs" />
<Compile Include="Clipping and Masking\SvgClipPath.cs" /> <Compile Include="Clipping and Masking\SvgClipPath.cs" />
<Compile Include="Clipping and Masking\SvgMask.cs" /> <Compile Include="Clipping and Masking\SvgMask.cs" />
<Compile Include="DataTypes\ISvgSupportsCoordinateUnits.cs" />
<Compile Include="Painting\GenericBoundable.cs" />
<Compile Include="SvgNodeReader.cs" />
<Compile Include="Css\CssQuery.cs" /> <Compile Include="Css\CssQuery.cs" />
<Compile Include="Css\SvgElementOps.cs" /> <Compile Include="Css\SvgElementOps.cs" />
<Compile Include="DataTypes\SvgAspectRatioConverter.cs" /> <Compile Include="DataTypes\SvgAspectRatioConverter.cs" />
...@@ -227,6 +231,7 @@ ...@@ -227,6 +231,7 @@
<Compile Include="SvgContentNode.cs" /> <Compile Include="SvgContentNode.cs" />
<Compile Include="SvgDefinitionDefaults.cs" /> <Compile Include="SvgDefinitionDefaults.cs" />
<Compile Include="NonSvgElement.cs" /> <Compile Include="NonSvgElement.cs" />
<Compile Include="SvgReader.cs" />
<Compile Include="SvgUnknownElement.cs" /> <Compile Include="SvgUnknownElement.cs" />
<Compile Include="SvgElementAttribute.cs" /> <Compile Include="SvgElementAttribute.cs" />
<Compile Include="SvgExtentions.cs" /> <Compile Include="SvgExtentions.cs" />
......
 
Microsoft Visual Studio Solution File, Format Version 11.00 Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010 # Visual Studio 2010
# SharpDevelop 4.4
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svg", "Svg.csproj", "{886A98C5-37C0-4E8B-885E-30C1D2F98B47}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svg", "Svg.csproj", "{886A98C5-37C0-4E8B-885E-30C1D2F98B47}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVGViewer", "..\Samples\SVGViewer\SVGViewer.csproj", "{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVGViewer", "..\Samples\SVGViewer\SVGViewer.csproj", "{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svg.UnitTests", "..\Tests\Svg.UnitTests\Svg.UnitTests.csproj", "{E702EB7D-D01D-438A-BADD-E72D4E49109F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A417AF1E-BEDB-4715-B4FD-D795579217F9}"
ProjectSection(SolutionItems) = preProject
Local.testsettings = Local.testsettings
Svg.vsmdi = Svg.vsmdi
TraceAndTestImpact.testsettings = TraceAndTestImpact.testsettings
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SvgW3CTestRunner", "..\Tests\SvgW3CTestRunner\SvgW3CTestRunner.csproj", "{8ED74C39-6CFF-421E-952A-30F9E6957108}"
EndProject
Global Global
GlobalSection(TestCaseManagementSettings) = postSolution
CategoryFile = Svg.vsmdi
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|Mixed Platforms = Debug|Mixed Platforms
Debug|x86 = Debug|x86 Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|Mixed Platforms = Release|Mixed Platforms
Release|x86 = Release|x86 Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Debug|Any CPU.Build.0 = Debug|Any CPU {886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Debug|x86.ActiveCfg = Debug|Any CPU {886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Debug|x86.ActiveCfg = Debug|Any CPU
{886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Release|Any CPU.ActiveCfg = Release|Any CPU {886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Release|Any CPU.Build.0 = Release|Any CPU {886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Release|Any CPU.Build.0 = Release|Any CPU
{886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Release|x86.ActiveCfg = Release|Any CPU {886A98C5-37C0-4E8B-885E-30C1D2F98B47}.Release|x86.ActiveCfg = Release|Any CPU
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Debug|x86.Build.0 = Debug|Any CPU {1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Debug|Mixed Platforms.Build.0 = Debug|x86
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Debug|x86.ActiveCfg = Debug|x86 {1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Debug|x86.ActiveCfg = Debug|x86
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Release|Any CPU.Build.0 = Release|Any CPU {1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Debug|x86.Build.0 = Debug|Any CPU
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Release|Any CPU.ActiveCfg = Release|Any CPU {1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Release|x86.Build.0 = Release|Any CPU {1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Release|Any CPU.Build.0 = Release|Any CPU
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Release|Mixed Platforms.ActiveCfg = Release|x86
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Release|Mixed Platforms.Build.0 = Release|x86
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Release|x86.ActiveCfg = Release|Any CPU {1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Release|x86.ActiveCfg = Release|Any CPU
{1B8F3C8A-CCAC-474E-B09D-522FBA93DCFD}.Release|x86.Build.0 = Release|Any CPU
{E702EB7D-D01D-438A-BADD-E72D4E49109F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E702EB7D-D01D-438A-BADD-E72D4E49109F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E702EB7D-D01D-438A-BADD-E72D4E49109F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{E702EB7D-D01D-438A-BADD-E72D4E49109F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{E702EB7D-D01D-438A-BADD-E72D4E49109F}.Debug|x86.ActiveCfg = Debug|Any CPU
{E702EB7D-D01D-438A-BADD-E72D4E49109F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E702EB7D-D01D-438A-BADD-E72D4E49109F}.Release|Any CPU.Build.0 = Release|Any CPU
{E702EB7D-D01D-438A-BADD-E72D4E49109F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{E702EB7D-D01D-438A-BADD-E72D4E49109F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{E702EB7D-D01D-438A-BADD-E72D4E49109F}.Release|x86.ActiveCfg = Release|Any CPU
{8ED74C39-6CFF-421E-952A-30F9E6957108}.Debug|Any CPU.ActiveCfg = Debug|x86
{8ED74C39-6CFF-421E-952A-30F9E6957108}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{8ED74C39-6CFF-421E-952A-30F9E6957108}.Debug|Mixed Platforms.Build.0 = Debug|x86
{8ED74C39-6CFF-421E-952A-30F9E6957108}.Debug|x86.ActiveCfg = Debug|x86
{8ED74C39-6CFF-421E-952A-30F9E6957108}.Debug|x86.Build.0 = Debug|x86
{8ED74C39-6CFF-421E-952A-30F9E6957108}.Release|Any CPU.ActiveCfg = Release|x86
{8ED74C39-6CFF-421E-952A-30F9E6957108}.Release|Mixed Platforms.ActiveCfg = Release|x86
{8ED74C39-6CFF-421E-952A-30F9E6957108}.Release|Mixed Platforms.Build.0 = Release|x86
{8ED74C39-6CFF-421E-952A-30F9E6957108}.Release|x86.ActiveCfg = Release|x86
{8ED74C39-6CFF-421E-952A-30F9E6957108}.Release|x86.Build.0 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
......
<?xml version="1.0" encoding="UTF-8"?>
<TestLists xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<TestList name="Lists of Tests" id="8c43106b-9dc1-4907-a29f-aa66a61bf5b6">
<RunConfiguration id="616d39c5-86e4-40f8-97e9-b8a423ce7322" name="Local" storage="local.testsettings" type="Microsoft.VisualStudio.TestTools.Common.TestRunConfiguration, Microsoft.VisualStudio.QualityTools.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</TestList>
</TestLists>
\ No newline at end of file
...@@ -63,9 +63,12 @@ namespace Svg ...@@ -63,9 +63,12 @@ namespace Svg
/// <returns>The attribute value if available; otherwise the ancestors value for the same attribute; otherwise the default value of <typeparamref name="TAttributeType"/>.</returns> /// <returns>The attribute value if available; otherwise the ancestors value for the same attribute; otherwise the default value of <typeparamref name="TAttributeType"/>.</returns>
public TAttributeType GetInheritedAttribute<TAttributeType>(string attributeName) public TAttributeType GetInheritedAttribute<TAttributeType>(string attributeName)
{ {
if (this.ContainsKey(attributeName) /*&& base[attributeName] != null*/) if (this.ContainsKey(attributeName) && !IsInheritValue(base[attributeName]))
{ {
return (TAttributeType)base[attributeName]; var result = (TAttributeType)base[attributeName];
var deferred = result as SvgDeferredPaintServer;
if (deferred != null) deferred.EnsureServer(_owner);
return result;
} }
if (this._owner.Parent != null) if (this._owner.Parent != null)
...@@ -79,6 +82,16 @@ namespace Svg ...@@ -79,6 +82,16 @@ namespace Svg
return default(TAttributeType); return default(TAttributeType);
} }
private bool IsInheritValue(object value)
{
return (value == null ||
(value is SvgFontStyle && (SvgFontStyle)value == SvgFontStyle.inherit) ||
(value is SvgFontWeight && (SvgFontWeight)value == SvgFontWeight.inherit) ||
(value is SvgTextAnchor && (SvgTextAnchor)value == SvgTextAnchor.inherit) ||
(value == "inherit")
);
}
/// <summary> /// <summary>
/// Gets the attribute with the specified name. /// Gets the attribute with the specified name.
/// </summary> /// </summary>
......
...@@ -30,6 +30,8 @@ namespace Svg ...@@ -30,6 +30,8 @@ namespace Svg
Ppi = PointsPerInch; Ppi = PointsPerInch;
} }
public Uri BaseUri { get; set; }
/// <summary> /// <summary>
/// Gets an <see cref="SvgElementIdManager"/> for this document. /// Gets an <see cref="SvgElementIdManager"/> for this document.
/// </summary> /// </summary>
...@@ -157,7 +159,9 @@ namespace Svg ...@@ -157,7 +159,9 @@ namespace Svg
throw new FileNotFoundException("The specified document cannot be found.", path); throw new FileNotFoundException("The specified document cannot be found.", path);
} }
return Open<T>(File.OpenRead(path), entities); var doc = Open<T>(File.OpenRead(path), entities);
doc.BaseUri = new Uri(System.IO.Path.GetFullPath(path));
return doc;
} }
/// <summary> /// <summary>
...@@ -305,9 +309,23 @@ namespace Svg ...@@ -305,9 +309,23 @@ namespace Svg
var cssTotal = styles.Select((s) => s.Content).Aggregate((p, c) => p + Environment.NewLine + c); var cssTotal = styles.Select((s) => s.Content).Aggregate((p, c) => p + Environment.NewLine + c);
var cssParser = new Parser(); var cssParser = new Parser();
var sheet = cssParser.Parse(cssTotal); var sheet = cssParser.Parse(cssTotal);
AggregateSelectorList aggList;
IEnumerable<BaseSelector> selectors;
IEnumerable<SvgElement> elemsToStyle; IEnumerable<SvgElement> elemsToStyle;
foreach (var rule in sheet.StyleRules) foreach (var rule in sheet.StyleRules)
{
aggList = rule.Selector as AggregateSelectorList;
if (aggList != null && aggList.Delimiter == ",")
{
selectors = aggList;
}
else
{
selectors = Enumerable.Repeat(rule.Selector, 1);
}
foreach (var selector in selectors)
{ {
elemsToStyle = svgDocument.QuerySelectorAll(rule.Selector.ToString()); elemsToStyle = svgDocument.QuerySelectorAll(rule.Selector.ToString());
foreach (var elem in elemsToStyle) foreach (var elem in elemsToStyle)
...@@ -319,8 +337,9 @@ namespace Svg ...@@ -319,8 +337,9 @@ namespace Svg
} }
} }
} }
}
FlushStyles(svgDocument); if (svgDocument != null) FlushStyles(svgDocument);
return svgDocument; return svgDocument;
} }
...@@ -345,10 +364,8 @@ namespace Svg ...@@ -345,10 +364,8 @@ namespace Svg
throw new ArgumentNullException("document"); throw new ArgumentNullException("document");
} }
using (var stream = new MemoryStream(UTF8Encoding.Default.GetBytes(document.InnerXml))) var reader = new SvgNodeReader(document.DocumentElement, null);
{ return Open<SvgDocument>(reader);
return Open<SvgDocument>(stream, null);
}
} }
public static Bitmap OpenAsBitmap(string path) public static Bitmap OpenAsBitmap(string path)
...@@ -373,6 +390,7 @@ namespace Svg ...@@ -373,6 +390,7 @@ namespace Svg
throw new ArgumentNullException("renderer"); throw new ArgumentNullException("renderer");
} }
renderer.Boundable(this);
this.Render(renderer); this.Render(renderer);
} }
...@@ -388,7 +406,9 @@ namespace Svg ...@@ -388,7 +406,9 @@ namespace Svg
throw new ArgumentNullException("graphics"); throw new ArgumentNullException("graphics");
} }
this.Render(SvgRenderer.FromGraphics(graphics)); var renderer = SvgRenderer.FromGraphics(graphics);
renderer.Boundable(this);
this.Render(renderer);
} }
/// <summary> /// <summary>
...@@ -427,6 +447,7 @@ namespace Svg ...@@ -427,6 +447,7 @@ namespace Svg
{ {
using (var renderer = SvgRenderer.FromImage(bitmap)) using (var renderer = SvgRenderer.FromImage(bitmap))
{ {
renderer.Boundable(this);
renderer.TextRenderingHint = TextRenderingHint.AntiAlias; renderer.TextRenderingHint = TextRenderingHint.AntiAlias;
renderer.TextContrast = 1; renderer.TextContrast = 1;
renderer.PixelOffsetMode = PixelOffsetMode.Half; renderer.PixelOffsetMode = PixelOffsetMode.Half;
......
...@@ -40,6 +40,7 @@ namespace Svg ...@@ -40,6 +40,7 @@ namespace Svg
private EventHandlerList _eventHandlers; private EventHandlerList _eventHandlers;
private SvgElementCollection _children; private SvgElementCollection _children;
private static readonly object _loadEventKey = new object(); private static readonly object _loadEventKey = new object();
private Region _graphicsClip;
private Matrix _graphicsMatrix; private Matrix _graphicsMatrix;
private SvgCustomAttributeCollection _customAttributes; private SvgCustomAttributeCollection _customAttributes;
private List<ISvgNode> _nodes = new List<ISvgNode>(); private List<ISvgNode> _nodes = new List<ISvgNode>();
...@@ -89,6 +90,16 @@ namespace Svg ...@@ -89,6 +90,16 @@ namespace Svg
internal set { this._elementName = value; } internal set { this._elementName = value; }
} }
/// <summary>
/// Gets or sets the color <see cref="SvgPaintServer"/> of this element which drives the currentColor property.
/// </summary>
[SvgAttribute("color")]
public virtual SvgPaintServer Color
{
get { return (this.Attributes["color"] == null) ? SvgColourServer.NotSet : (SvgPaintServer)this.Attributes["color"]; }
set { this.Attributes["color"] = value; }
}
/// <summary> /// <summary>
/// Gets or sets the content of the element. /// Gets or sets the content of the element.
/// </summary> /// </summary>
...@@ -247,15 +258,17 @@ namespace Svg ...@@ -247,15 +258,17 @@ namespace Svg
/// Applies the required transforms to <see cref="SvgRenderer"/>. /// Applies the required transforms to <see cref="SvgRenderer"/>.
/// </summary> /// </summary>
/// <param name="renderer">The <see cref="SvgRenderer"/> to be transformed.</param> /// <param name="renderer">The <see cref="SvgRenderer"/> to be transformed.</param>
protected internal virtual void PushTransforms(SvgRenderer renderer) protected internal virtual bool PushTransforms(SvgRenderer renderer)
{ {
_graphicsMatrix = renderer.Transform; _graphicsMatrix = renderer.Transform;
_graphicsClip = renderer.Clip;
// Return if there are no transforms // Return if there are no transforms
if (this.Transforms == null || this.Transforms.Count == 0) if (this.Transforms == null || this.Transforms.Count == 0)
{ {
return; return true;
} }
if (this.Transforms.Count == 1 && this.Transforms[0].Matrix.Equals(new Matrix(0, 0, 0, 0, 0, 0))) return false;
Matrix transformMatrix = renderer.Transform; Matrix transformMatrix = renderer.Transform;
...@@ -265,6 +278,8 @@ namespace Svg ...@@ -265,6 +278,8 @@ namespace Svg
} }
renderer.Transform = transformMatrix; renderer.Transform = transformMatrix;
return true;
} }
/// <summary> /// <summary>
...@@ -275,6 +290,8 @@ namespace Svg ...@@ -275,6 +290,8 @@ namespace Svg
{ {
renderer.Transform = _graphicsMatrix; renderer.Transform = _graphicsMatrix;
_graphicsMatrix = null; _graphicsMatrix = null;
renderer.SetClip(_graphicsClip);
_graphicsClip = null;
} }
/// <summary> /// <summary>
...@@ -659,7 +676,7 @@ namespace Svg ...@@ -659,7 +676,7 @@ namespace Svg
{ {
if(!(child is SvgGroup)) if(!(child is SvgGroup))
{ {
var childPath = ((SvgVisualElement)child).Path; var childPath = ((SvgVisualElement)child).Path(null);
if (childPath != null) if (childPath != null)
{ {
...@@ -672,7 +689,7 @@ namespace Svg ...@@ -672,7 +689,7 @@ namespace Svg
} }
} }
AddPaths(child, path); if (!(child is SvgPaintServer)) AddPaths(child, path);
} }
} }
...@@ -681,7 +698,7 @@ namespace Svg ...@@ -681,7 +698,7 @@ namespace Svg
/// </summary> /// </summary>
/// <param name="elem"></param> /// <param name="elem"></param>
/// <param name="path"></param> /// <param name="path"></param>
protected GraphicsPath GetPaths(SvgElement elem) protected GraphicsPath GetPaths(SvgElement elem, SvgRenderer renderer)
{ {
var ret = new GraphicsPath(); var ret = new GraphicsPath();
...@@ -691,7 +708,7 @@ namespace Svg ...@@ -691,7 +708,7 @@ namespace Svg
{ {
if(!(child is SvgGroup)) if(!(child is SvgGroup))
{ {
var childPath = ((SvgVisualElement)child).Path; var childPath = ((SvgVisualElement)child).Path(renderer);
if (childPath != null) if (childPath != null)
{ {
...@@ -704,7 +721,7 @@ namespace Svg ...@@ -704,7 +721,7 @@ namespace Svg
} }
else else
{ {
var childPath = GetPaths(child); var childPath = GetPaths(child, renderer);
if(child.Transforms != null) if(child.Transforms != null)
childPath.Transform(child.Transforms.GetMatrix()); childPath.Transform(child.Transforms.GetMatrix());
} }
......
...@@ -130,40 +130,6 @@ namespace Svg ...@@ -130,40 +130,6 @@ namespace Svg
while (reader.MoveToNextAttribute()) while (reader.MoveToNextAttribute())
{ {
//// Special treatment for "style"
//if (reader.LocalName.Equals("style") && !(element is NonSvgElement))
//{
// styles = reader.Value.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
// for (i = 0; i < styles.Length; i++)
// {
// if (!styles[i].Contains(":"))
// {
// continue;
// }
// style = styles[i].Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
// SetPropertyValue(element, style[0].Trim(), style[1].Trim(), document);
// }
// //defaults for text can come from the document
// if (element.ElementName == "text")
// {
// if (!styles.Contains("font-size") && document.CustomAttributes.ContainsKey("font-size") && document.CustomAttributes["font-size"] != null)
// {
// SetPropertyValue(element, "font-size", document.CustomAttributes["font-size"], document);
// }
// if (!styles.Contains("font-family") && document.CustomAttributes.ContainsKey("font-family") && document.CustomAttributes["font-family"] != null)
// {
// SetPropertyValue(element, "font-family", document.CustomAttributes["font-family"], document);
// }
// }
// continue;
//}
//SetPropertyValue(element, reader.LocalName, reader.Value, document);
if (reader.LocalName.Equals("style") && !(element is NonSvgElement)) if (reader.LocalName.Equals("style") && !(element is NonSvgElement))
{ {
var inlineSheet = cssParser.Parse("#a{" + reader.Value + "}"); var inlineSheet = cssParser.Parse("#a{" + reader.Value + "}");
...@@ -175,15 +141,89 @@ namespace Svg ...@@ -175,15 +141,89 @@ namespace Svg
} }
} }
} }
else else if (IsStyleAttribute(reader.LocalName))
{ {
element.AddStyle(reader.LocalName, reader.Value, 2 << 16); element.AddStyle(reader.LocalName, reader.Value, 2 << 16);
} }
else
{
SetPropertyValue(element, reader.LocalName, reader.Value, document);
}
} }
//Trace.TraceInformation("End SetAttributes"); //Trace.TraceInformation("End SetAttributes");
} }
private static bool IsStyleAttribute(string name)
{
switch (name)
{
case "alignment-baseline":
case "baseline-shift":
case "clip":
case "clip-path":
case "clip-rule":
case "color":
case "color-interpolation":
case "color-interpolation-filters":
case "color-profile":
case "color-rendering":
case "cursor":
case "direction":
case "display":
case "dominant-baseline":
case "enable-background":
case "fill":
case "fill-opacity":
case "fill-rule":
case "filter":
case "flood-color":
case "flood-opacity":
case "font":
case "font-family":
case "font-size":
case "font-size-adjust":
case "font-stretch":
case "font-style":
case "font-variant":
case "font-weight":
case "glyph-orientation-horizontal":
case "glyph-orientation-vertical":
case "image-rendering":
case "kerning":
case "letter-spacing":
case "lighting-color":
case "marker":
case "marker-end":
case "marker-mid":
case "marker-start":
case "mask":
case "opacity":
case "overflow":
case "pointer-events":
case "shape-rendering":
case "stop-color":
case "stop-opacity":
case "stroke":
case "stroke-dasharray":
case "stroke-dashoffset":
case "stroke-linecap":
case "stroke-linejoin":
case "stroke-miterlimit":
case "stroke-opacity":
case "stroke-width":
case "text-anchor":
case "text-decoration":
case "text-rendering":
case "unicode-bidi":
case "visibility":
case "word-spacing":
case "writing-mode":
return true;
}
return false;
}
private static Dictionary<Type, Dictionary<string, PropertyDescriptorCollection>> _propertyDescriptors = new Dictionary<Type, Dictionary<string, PropertyDescriptorCollection>>(); private static Dictionary<Type, Dictionary<string, PropertyDescriptorCollection>> _propertyDescriptors = new Dictionary<Type, Dictionary<string, PropertyDescriptorCollection>>();
private static object syncLock = new object(); private static object syncLock = new object();
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.IO;
using System.Collections.Specialized;
namespace Svg
{
internal sealed class SvgNodeReader : XmlNodeReader
{
private Dictionary<string, string> _entities;
private string _value;
private bool _customValue = false;
private string _localName;
public SvgNodeReader(XmlNode node, Dictionary<string, string> entities)
: base(node)
{
this._entities = entities;
}
/// <summary>
/// Gets the text value of the current node.
/// </summary>
/// <value></value>
/// <returns>The value returned depends on the <see cref="P:System.Xml.XmlTextReader.NodeType"/> of the node. The following table lists node types that have a value to return. All other node types return String.Empty.Node Type Value AttributeThe value of the attribute. CDATAThe content of the CDATA section. CommentThe content of the comment. DocumentTypeThe internal subset. ProcessingInstructionThe entire content, excluding the target. SignificantWhitespaceThe white space within an xml:space= 'preserve' scope. TextThe content of the text node. WhitespaceThe white space between markup. XmlDeclarationThe content of the declaration. </returns>
public override string Value
{
get
{
return (this._customValue) ? this._value : base.Value;
}
}
/// <summary>
/// Gets the local name of the current node.
/// </summary>
/// <value></value>
/// <returns>The name of the current node with the prefix removed. For example, LocalName is book for the element &lt;bk:book&gt;.For node types that do not have a name (like Text, Comment, and so on), this property returns String.Empty.</returns>
public override string LocalName
{
get
{
return (this._customValue) ? this._localName : base.LocalName;
}
}
private IDictionary<string, string> Entities
{
get
{
if (this._entities == null)
{
this._entities = new Dictionary<string, string>();
}
return this._entities;
}
}
/// <summary>
/// Moves to the next attribute.
/// </summary>
/// <returns>
/// true if there is a next attribute; false if there are no more attributes.
/// </returns>
public override bool MoveToNextAttribute()
{
bool moved = base.MoveToNextAttribute();
if (moved)
{
this._localName = base.LocalName;
if (this.ReadAttributeValue())
{
if (this.NodeType == XmlNodeType.EntityReference)
{
this.ResolveEntity();
}
else
{
this._value = base.Value;
}
}
this._customValue = true;
}
return moved;
}
/// <summary>
/// Reads the next node from the stream.
/// </summary>
/// <returns>
/// true if the next node was read successfully; false if there are no more nodes to read.
/// </returns>
/// <exception cref="T:System.Xml.XmlException">An error occurred while parsing the XML. </exception>
public override bool Read()
{
this._customValue = false;
bool read = base.Read();
if (this.NodeType == XmlNodeType.DocumentType)
{
this.ParseEntities();
}
return read;
}
private void ParseEntities()
{
const string entityText = "<!ENTITY";
string[] entities = this.Value.Split(new string[]{entityText}, StringSplitOptions.None);
string[] parts = null;
string name = null;
string value = null;
foreach (string entity in entities)
{
if (string.IsNullOrEmpty(entity.Trim()))
{
continue;
}
parts = entity.Trim().Split(new char[]{' ', '\t'}, StringSplitOptions.RemoveEmptyEntries);
name = parts[0];
value = parts[1].Split(new char[] { this.QuoteChar }, StringSplitOptions.RemoveEmptyEntries)[0];
this.Entities.Add(name, value);
}
}
/// <summary>
/// Resolves the entity reference for EntityReference nodes.
/// </summary>
public override void ResolveEntity()
{
if (this.NodeType == XmlNodeType.EntityReference)
{
if (this._entities.ContainsKey(this.Name))
{
this._value = this._entities[this.Name];
}
else
{
this._value = string.Empty;
}
this._customValue = true;
}
}
}
}
\ No newline at end of file
...@@ -8,9 +8,26 @@ using System.Drawing.Text; ...@@ -8,9 +8,26 @@ using System.Drawing.Text;
namespace Svg namespace Svg
{ {
/// <summary>
/// Convenience wrapper around a graphics object
/// </summary>
public sealed class SvgRenderer : IDisposable public sealed class SvgRenderer : IDisposable
{ {
private Graphics _innerGraphics; private Graphics _innerGraphics;
private Stack<ISvgBoundable> _boundables = new Stack<ISvgBoundable>();
public void Boundable(ISvgBoundable boundable)
{
_boundables.Push(boundable);
}
public ISvgBoundable Boundable()
{
return _boundables.Peek();
}
public ISvgBoundable PopBoundable()
{
return _boundables.Pop();
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SvgRenderer"/> class. /// Initializes a new instance of the <see cref="SvgRenderer"/> class.
...@@ -47,6 +64,14 @@ namespace Svg ...@@ -47,6 +64,14 @@ namespace Svg
return renderer; return renderer;
} }
public static SvgRenderer FromNull()
{
SvgRenderer renderer = new SvgRenderer();
var img = new Bitmap(1, 1);
renderer._innerGraphics = Graphics.FromImage(img);
return renderer;
}
public void DrawImageUnscaled(Image image, Point location) public void DrawImageUnscaled(Image image, Point location)
{ {
this._innerGraphics.DrawImageUnscaled(image, location); this._innerGraphics.DrawImageUnscaled(image, location);
...@@ -57,9 +82,13 @@ namespace Svg ...@@ -57,9 +82,13 @@ namespace Svg
_innerGraphics.DrawImage(image, destRect, srcRect, graphicsUnit); _innerGraphics.DrawImage(image, destRect, srcRect, graphicsUnit);
} }
public void AddClip(Region region)
{
this._innerGraphics.SetClip(region, CombineMode.Intersect);
}
public void SetClip(Region region) public void SetClip(Region region)
{ {
this._innerGraphics.SetClip(region, CombineMode.Complement); this._innerGraphics.SetClip(region, CombineMode.Replace);
} }
public void FillPath(Brush brush, GraphicsPath path) public void FillPath(Brush brush, GraphicsPath path)
...@@ -151,9 +180,10 @@ namespace Svg ...@@ -151,9 +180,10 @@ namespace Svg
public SizeF MeasureString(string text, Font font) public SizeF MeasureString(string text, Font font)
{ {
var ff = font.FontFamily; var ff = font.FontFamily;
float lineSpace = ff.GetLineSpacing(font.Style); //Baseline calculation to match http://bobpowell.net/formattingtext.aspx
float ascent = ff.GetCellAscent(font.Style); float ascent = ff.GetCellAscent(font.Style);
float baseline = font.GetHeight(this._innerGraphics) * ascent / lineSpace; float baselineOffset = font.SizeInPoints / ff.GetEmHeight(font.Style) * ascent;
float baselineOffsetPixels = this._innerGraphics.DpiY / 72f * baselineOffset;
StringFormat format = StringFormat.GenericTypographic; StringFormat format = StringFormat.GenericTypographic;
format.SetMeasurableCharacterRanges(new CharacterRange[]{new CharacterRange(0, text.Length)}); format.SetMeasurableCharacterRanges(new CharacterRange[]{new CharacterRange(0, text.Length)});
...@@ -161,7 +191,7 @@ namespace Svg ...@@ -161,7 +191,7 @@ namespace Svg
Region[] r = this._innerGraphics.MeasureCharacterRanges(text, font, new Rectangle(0, 0, 1000, 1000), format); Region[] r = this._innerGraphics.MeasureCharacterRanges(text, font, new Rectangle(0, 0, 1000, 1000), format);
RectangleF rect = r[0].GetBounds(this._innerGraphics); RectangleF rect = r[0].GetBounds(this._innerGraphics);
return new SizeF(rect.Width, baseline); return new SizeF(rect.Width, baselineOffsetPixels);
} }
} }
} }
\ No newline at end of file
...@@ -18,14 +18,14 @@ namespace Svg ...@@ -18,14 +18,14 @@ namespace Svg
public SvgTextReader(Stream stream, Dictionary<string, string> entities) public SvgTextReader(Stream stream, Dictionary<string, string> entities)
: base(stream) : base(stream)
{ {
this.EntityHandling = EntityHandling.ExpandCharEntities; this.EntityHandling = EntityHandling.ExpandEntities;
this._entities = entities; this._entities = entities;
} }
public SvgTextReader(TextReader reader, Dictionary<string, string> entities) public SvgTextReader(TextReader reader, Dictionary<string, string> entities)
: base(reader) : base(reader)
{ {
this.EntityHandling = EntityHandling.ExpandCharEntities; this.EntityHandling = EntityHandling.ExpandEntities;
this._entities = entities; this._entities = entities;
} }
...@@ -126,6 +126,7 @@ namespace Svg ...@@ -126,6 +126,7 @@ namespace Svg
string[] parts = null; string[] parts = null;
string name = null; string name = null;
string value = null; string value = null;
int quoteIndex;
foreach (string entity in entities) foreach (string entity in entities)
{ {
...@@ -134,13 +135,16 @@ namespace Svg ...@@ -134,13 +135,16 @@ namespace Svg
continue; continue;
} }
parts = entity.Trim().Split(new char[]{' ', '\t'}, StringSplitOptions.RemoveEmptyEntries); name = entity.Trim();
name = parts[0]; quoteIndex = name.IndexOf(this.QuoteChar);
value = parts[1].Split(new char[] { this.QuoteChar }, StringSplitOptions.RemoveEmptyEntries)[0]; if (quoteIndex > 0)
{
value = name.Substring(quoteIndex + 1, name.LastIndexOf(this.QuoteChar) - quoteIndex - 1);
name = name.Substring(0, quoteIndex).Trim();
this.Entities.Add(name, value); this.Entities.Add(name, value);
} }
} }
}
/// <summary> /// <summary>
/// Resolves the entity reference for EntityReference nodes. /// Resolves the entity reference for EntityReference nodes.
......
...@@ -25,9 +25,7 @@ namespace Svg ...@@ -25,9 +25,7 @@ namespace Svg
private SvgUnitCollection _dx = new SvgUnitCollection(); private SvgUnitCollection _dx = new SvgUnitCollection();
private SvgUnit _letterSpacing; private SvgUnit _letterSpacing;
private SvgUnit _wordSpacing; private SvgUnit _wordSpacing;
private SvgTextAnchor _textAnchor = SvgTextAnchor.inherit;
private static readonly SvgRenderer _stringMeasure; private static readonly SvgRenderer _stringMeasure;
private const string DefaultFontFamily = "Times New Roman";
private XmlSpaceHandling _space = XmlSpaceHandling.@default; private XmlSpaceHandling _space = XmlSpaceHandling.@default;
...@@ -57,8 +55,15 @@ namespace Svg ...@@ -57,8 +55,15 @@ namespace Svg
[SvgAttribute("text-anchor")] [SvgAttribute("text-anchor")]
public virtual SvgTextAnchor TextAnchor public virtual SvgTextAnchor TextAnchor
{ {
get { return this._textAnchor; } get { return (this.Attributes["text-anchor"] == null) ? SvgTextAnchor.inherit : (SvgTextAnchor)this.Attributes["text-anchor"]; }
set { this._textAnchor = value; this.IsPathDirty = true; } set { this.Attributes["text-anchor"] = value; this.IsPathDirty = true; }
}
[SvgAttribute("baseline-shift")]
public virtual string BaselineShift
{
get { return this.Attributes["baseline-shift"] as string; }
set { this.Attributes["baseline-shift"] = value; this.IsPathDirty = true; }
} }
/// <summary> /// <summary>
...@@ -166,7 +171,7 @@ namespace Svg ...@@ -166,7 +171,7 @@ namespace Svg
/// <value>The fill.</value> /// <value>The fill.</value>
public override SvgPaintServer Fill public override SvgPaintServer Fill
{ {
get { return (this.Attributes["fill"] == null) ? new SvgColourServer(Color.Black) : (SvgPaintServer)this.Attributes["fill"]; } get { return (this.Attributes["fill"] == null) ? new SvgColourServer(System.Drawing.Color.Black) : (SvgPaintServer)this.Attributes["fill"]; }
set { this.Attributes["fill"] = value; } set { this.Attributes["fill"] = value; }
} }
...@@ -198,33 +203,15 @@ namespace Svg ...@@ -198,33 +203,15 @@ namespace Svg
{ {
get get
{ {
var path = this.Path; var path = this.Path(null);
foreach (var elem in this.Children.OfType<SvgVisualElement>()) foreach (var elem in this.Children.OfType<SvgVisualElement>())
{ {
path.AddPath(elem.Path, false); path.AddPath(elem.Path(null), false);
} }
return path.GetBounds(); return path.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;
}
/// <summary> /// <summary>
/// Renders the <see cref="SvgElement"/> and contents to the specified <see cref="Graphics"/> object. /// Renders the <see cref="SvgElement"/> and contents to the specified <see cref="Graphics"/> object.
/// </summary> /// </summary>
...@@ -232,7 +219,7 @@ namespace Svg ...@@ -232,7 +219,7 @@ namespace Svg
/// <remarks>Necessary to make sure that any internal tspan elements get rendered as well</remarks> /// <remarks>Necessary to make sure that any internal tspan elements get rendered as well</remarks>
protected override void Render(SvgRenderer renderer) protected override void Render(SvgRenderer renderer)
{ {
if ((this.Path != null) && this.Visible && this.Displayable) if ((this.Path(renderer) != null) && this.Visible && this.Displayable)
{ {
this.PushTransforms(renderer); this.PushTransforms(renderer);
this.SetClip(renderer); this.SetClip(renderer);
...@@ -275,9 +262,9 @@ namespace Svg ...@@ -275,9 +262,9 @@ namespace Svg
} }
public SizeF Bounds { get; set; } public SizeF Bounds { get; set; }
} }
protected BoundsData GetTextBounds() protected BoundsData GetTextBounds(SvgRenderer renderer)
{ {
var font = GetFont(); var font = GetFont(renderer);
SvgTextBase innerText; SvgTextBase innerText;
SizeF stringBounds; SizeF stringBounds;
float totalHeight = 0; float totalHeight = 0;
...@@ -305,7 +292,8 @@ namespace Svg ...@@ -305,7 +292,8 @@ namespace Svg
{ {
Bounds = stringBounds, Bounds = stringBounds,
Node = new SvgContentNode() { Content = ch }, Node = new SvgContentNode() { Content = ch },
xOffset = (i == 0 ? 0 : _x[i].ToDeviceValue(this) - _x[0].ToDeviceValue(this)) xOffset = (i == 0 ? 0 : _x[i].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this) -
_x[0].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this))
}); });
} }
} }
...@@ -326,9 +314,9 @@ namespace Svg ...@@ -326,9 +314,9 @@ namespace Svg
} }
else else
{ {
stringBounds = innerText.GetTextBounds().Bounds; stringBounds = innerText.GetTextBounds(renderer).Bounds;
result.Nodes.Add(new NodeBounds() { Bounds = stringBounds, Node = node, xOffset = totalWidth }); result.Nodes.Add(new NodeBounds() { Bounds = stringBounds, Node = node, xOffset = totalWidth });
if (innerText.Dx.Count == 1) totalWidth += innerText.Dx[0].ToDeviceValue(this); if (innerText.Dx.Count == 1) totalWidth += innerText.Dx[0].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this);
} }
totalHeight = Math.Max(totalHeight, stringBounds.Height); totalHeight = Math.Max(totalHeight, stringBounds.Height);
totalWidth += stringBounds.Width; totalWidth += stringBounds.Width;
...@@ -345,9 +333,7 @@ namespace Svg ...@@ -345,9 +333,7 @@ namespace Svg
/// Gets the <see cref="GraphicsPath"/> for this element. /// Gets the <see cref="GraphicsPath"/> for this element.
/// </summary> /// </summary>
/// <value></value> /// <value></value>
public override System.Drawing.Drawing2D.GraphicsPath Path public override System.Drawing.Drawing2D.GraphicsPath Path(SvgRenderer renderer)
{
get
{ {
// Make sure the path is always null if there is no text // 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 there is a TSpan inside of this text element then path should not be null (even if this text is empty!)
...@@ -358,21 +344,22 @@ namespace Svg ...@@ -358,21 +344,22 @@ namespace Svg
if (_path == null || this.IsPathDirty) if (_path == null || this.IsPathDirty)
{ {
renderer = (renderer ?? SvgRenderer.FromNull());
// Measure the overall bounds of all the text // Measure the overall bounds of all the text
var boundsData = GetTextBounds(); var boundsData = GetTextBounds(renderer);
var font = GetFont(); var font = GetFont(renderer);
SvgTextBase innerText; SvgTextBase innerText;
float x = (_x.Count < 1 ? _calcX : _x[0].ToDeviceValue(this)) + (_dx.Count < 1 ? 0 : _dx[0].ToDeviceValue(this)); float x = (_x.Count < 1 ? _calcX : _x[0].ToDeviceValue(renderer, UnitRenderingType.HorizontalOffset, this)) +
float y = (_y.Count < 1 ? _calcY : _y[0].ToDeviceValue(this, true)) + (_dy.Count < 1 ? 0 : _dy[0].ToDeviceValue(this, true)); (_dx.Count < 1 ? 0 : _dx[0].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this));
float y = (_y.Count < 1 ? _calcY : _y[0].ToDeviceValue(renderer, UnitRenderingType.VerticalOffset, this)) +
(_dy.Count < 1 ? 0 : _dy[0].ToDeviceValue(renderer, UnitRenderingType.Vertical, this));
_path = new GraphicsPath(); _path = new GraphicsPath();
_path.StartFigure(); _path.StartFigure();
var anchorElem = (from e in this.ParentsAndSelf.OfType<SvgTextBase>() where e.TextAnchor != SvgTextAnchor.inherit select e).FirstOrDefault();
// Determine the location of the start point // Determine the location of the start point
switch (anchorElem == null ? this.TextAnchor : anchorElem.TextAnchor) switch (this.TextAnchor)
{ {
case SvgTextAnchor.Middle: case SvgTextAnchor.Middle:
x -= (boundsData.Bounds.Width / 2); x -= (boundsData.Bounds.Width / 2);
...@@ -382,6 +369,35 @@ namespace Svg ...@@ -382,6 +369,35 @@ namespace Svg
break; break;
} }
try
{
renderer.Boundable(new FontBoundable(font));
switch (this.BaselineShift)
{
case null:
case "":
case "baseline":
case "inherit":
// do nothing
break;
case "sub":
y += new SvgUnit(SvgUnitType.Ex, 1).ToDeviceValue(renderer, UnitRenderingType.Vertical, this);
break;
case "super":
y -= new SvgUnit(SvgUnitType.Ex, 1).ToDeviceValue(renderer, UnitRenderingType.Vertical, this);
break;
default:
var convert = new SvgUnitConverter();
var shift = (SvgUnit)convert.ConvertFromInvariantString(this.BaselineShift);
y -= shift.ToDeviceValue(renderer, UnitRenderingType.Vertical, this);
break;
}
}
finally
{
renderer.PopBoundable();
}
NodeBounds data; NodeBounds data;
var yCummOffset = 0.0f; var yCummOffset = 0.0f;
for (var i = 0; i < boundsData.Nodes.Count; i++) for (var i = 0; i < boundsData.Nodes.Count; i++)
...@@ -391,7 +407,7 @@ namespace Svg ...@@ -391,7 +407,7 @@ namespace Svg
if (innerText == null) if (innerText == null)
{ {
// Minus FontSize because the x/y coords mark the bottom left, not bottom top. // Minus FontSize because the x/y coords mark the bottom left, not bottom top.
DrawString(_path, x + data.xOffset, y - boundsData.Bounds.Height, font, DrawString(renderer, _path, x + data.xOffset, y - boundsData.Bounds.Height, font,
PrepareText(data.Node.Content, i > 0 && boundsData.Nodes[i - 1].Node is SvgTextBase, 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)); i < boundsData.Nodes.Count - 1 && boundsData.Nodes[i + 1].Node is SvgTextBase));
} }
...@@ -399,7 +415,7 @@ namespace Svg ...@@ -399,7 +415,7 @@ namespace Svg
{ {
innerText._calcX = x + data.xOffset; innerText._calcX = x + data.xOffset;
innerText._calcY = y + yCummOffset; innerText._calcY = y + yCummOffset;
if (innerText.Dy.Count == 1) yCummOffset += innerText.Dy[0].ToDeviceValue(this); if (innerText.Dy.Count == 1) yCummOffset += innerText.Dy[0].ToDeviceValue(renderer, UnitRenderingType.Vertical, this);
} }
} }
...@@ -408,11 +424,8 @@ namespace Svg ...@@ -408,11 +424,8 @@ namespace Svg
} }
return _path; return _path;
} }
protected set
{ private static readonly Regex MultipleSpaces = new Regex(@" {2,}", RegexOptions.Compiled);
_path = value;
}
}
/// <summary> /// <summary>
/// Prepare the text according to the whitespace handling rules. <see href="http://www.w3.org/TR/SVG/text.html">SVG Spec</see>. /// Prepare the text according to the whitespace handling rules. <see href="http://www.w3.org/TR/SVG/text.html">SVG Spec</see>.
...@@ -423,82 +436,21 @@ namespace Svg ...@@ -423,82 +436,21 @@ namespace Svg
{ {
if (_space == XmlSpaceHandling.preserve) if (_space == XmlSpaceHandling.preserve)
{ {
return (leadingSpace ? " " : "") + value.Replace('\t', ' ').Replace("\r\n", " ").Replace('\r', ' ').Replace('\n', ' ') + (trailingSpace ? " " : ""); return value.Replace('\t', ' ').Replace("\r\n", " ").Replace('\r', ' ').Replace('\n', ' ');
} }
else else
{ {
return (leadingSpace ? " " : "") + value.Replace("\r", "").Replace("\n", "").Replace('\t', ' ').Trim().Replace(" ", " ") + (trailingSpace ? " " : ""); var convValue = MultipleSpaces.Replace(value.Replace("\r", "").Replace("\n", "").Replace('\t', ' '), " ");
if (!leadingSpace) convValue = convValue.TrimStart();
if (!trailingSpace) convValue = convValue.TrimEnd();
return convValue;
} }
} }
/// <summary>
/// Get the font information based on data stored with the text object or inherited from the parent.
/// </summary>
/// <returns></returns>
internal Font GetFont()
{
var parentList = this.ParentsAndSelf.OfType<SvgVisualElement>().ToList();
// Get the font-size
float fontSize;
var fontSizeUnit = GetInheritedFontSize();
if (fontSizeUnit == SvgUnit.None)
{
fontSize = 1.0f;
}
else
{
fontSize = fontSizeUnit.ToDeviceValue(this);
}
var fontStyle = System.Drawing.FontStyle.Regular;
// Get the font-weight
var weightElement = (from e in parentList where e.FontWeight != SvgFontWeight.inherit select e).FirstOrDefault();
if (weightElement != null)
{
switch (weightElement.FontWeight)
{
case SvgFontWeight.bold:
case SvgFontWeight.bolder:
case SvgFontWeight.w700:
case SvgFontWeight.w800:
case SvgFontWeight.w900:
fontStyle |= System.Drawing.FontStyle.Bold;
break;
}
}
// Get the font-style
var styleElement = (from e in parentList where e.FontStyle != SvgFontStyle.inherit select e).FirstOrDefault();
if (styleElement != null)
{
switch (styleElement.FontStyle)
{
case SvgFontStyle.italic:
case SvgFontStyle.oblique:
fontStyle |= System.Drawing.FontStyle.Italic;
break;
}
}
// Get the font-family
var fontFamilyElement = (from e in parentList where e.FontFamily != null && e.FontFamily != "inherit" select e).FirstOrDefault();
string family;
if (fontFamilyElement == null)
{
family = DefaultFontFamily;
}
else
{
family = ValidateFontFamily(fontFamilyElement.FontFamily) ?? DefaultFontFamily;
}
return new Font(family, fontSize, fontStyle, GraphicsUnit.Pixel);
}
/// <summary> /// <summary>
/// Draws a string on a path at a specified location and with a specified font. /// Draws a string on a path at a specified location and with a specified font.
/// </summary> /// </summary>
internal void DrawString(GraphicsPath path, float x, float y, Font font, string text) internal void DrawString(SvgRenderer renderer, GraphicsPath path, float x, float y, Font font, string text)
{ {
PointF location = new PointF(x, y); PointF location = new PointF(x, y);
...@@ -507,8 +459,8 @@ namespace Svg ...@@ -507,8 +459,8 @@ namespace Svg
{ {
// Cut up into words, or just leave as required // Cut up into words, or just leave as required
string[] words = (this.WordSpacing.Value > 0.0f) ? text.Split(' ') : new string[] { text }; string[] words = (this.WordSpacing.Value > 0.0f) ? text.Split(' ') : new string[] { text };
float wordSpacing = this.WordSpacing.ToDeviceValue(this); float wordSpacing = this.WordSpacing.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this);
float letterSpacing = this.LetterSpacing.ToDeviceValue(this); float letterSpacing = this.LetterSpacing.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this);
float start = x; float start = x;
foreach (string word in words) foreach (string word in words)
...@@ -579,5 +531,30 @@ namespace Svg ...@@ -579,5 +531,30 @@ namespace Svg
} }
#endif #endif
private class FontBoundable : ISvgBoundable
{
private Font _font;
public FontBoundable(Font font)
{
_font = font;
}
public PointF Location
{
get { return PointF.Empty; }
}
public SizeF Size
{
get { return new SizeF(1, _font.Size); }
}
public RectangleF Bounds
{
get { return new RectangleF(this.Location, this.Size); }
}
}
} }
} }
<?xml version="1.0" encoding="UTF-8"?>
<TestSettings name="Trace and Test Impact" id="23149ffc-ac6c-4c10-b846-c450c14dcd05" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Description>These are test settings for Trace and Test Impact.</Description>
<Execution>
<TestTypeSpecific />
<AgentRule name="Execution Agents">
</AgentRule>
</Execution>
</TestSettings>
\ No newline at end of file
...@@ -21,7 +21,8 @@ namespace Svg.Transforms ...@@ -21,7 +21,8 @@ namespace Svg.Transforms
if (transforms[i] == ')') if (transforms[i] == ')')
{ {
yield return transforms.Substring(transformEnd, i - transformEnd + 1).Trim(); yield return transforms.Substring(transformEnd, i - transformEnd + 1).Trim();
transformEnd = i + 1; while (i < transforms.Length && !char.IsLetter(transforms[i])) i++;
transformEnd = i;
} }
} }
} }
......
using Svg.Css;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using ExCSS;
namespace Svg.UnitTests
{
/// <summary>
///This is a test class for CssQueryTest and is intended
///to contain all CssQueryTest Unit Tests
///</summary>
[TestClass()]
public class CssQueryTest
{
private TestContext testContextInstance;
/// <summary>
///Gets or sets the test context which provides
///information about and functionality for the current test run.
///</summary>
public TestContext TestContext
{
get
{
return testContextInstance;
}
set
{
testContextInstance = value;
}
}
#region Additional test attributes
//
//You can use the following additional attributes as you write your tests:
//
//Use ClassInitialize to run code before running the first test in the class
//[ClassInitialize()]
//public static void MyClassInitialize(TestContext testContext)
//{
//}
//
//Use ClassCleanup to run code after all tests in a class have run
//[ClassCleanup()]
//public static void MyClassCleanup()
//{
//}
//
//Use TestInitialize to run code before running each test
//[TestInitialize()]
//public void MyTestInitialize()
//{
//}
//
//Use TestCleanup to run code after each test has run
//[TestCleanup()]
//public void MyTestCleanup()
//{
//}
//
#endregion
private void TestSelectorSpecificity(string selector, int specificity)
{
var parser = new ExCSS.Parser();
var sheet = parser.Parse(selector + " {color:black}");
Assert.AreEqual(specificity, CssQuery.GetSpecificity(sheet.StyleRules[0].Selector));
}
/// <summary>
///A test for GetSpecificity
///</summary>
///<remarks>Lifted from http://www.smashingmagazine.com/2007/07/27/css-specificity-things-you-should-know/, and http://css-tricks.com/specifics-on-css-specificity/ </remarks>
[TestMethod()]
public void RunSpecificityTests()
{
TestSelectorSpecificity("*", 0x0);
TestSelectorSpecificity("li", 0x10);
TestSelectorSpecificity("li:first-line", 0x20);
TestSelectorSpecificity("ul li", 0x20);
TestSelectorSpecificity("ul ol+li", 0x30);
TestSelectorSpecificity("h1 + *[rel=up]", 0x110);
TestSelectorSpecificity("ul ol li.red", 0x130);
TestSelectorSpecificity("li.red.level", 0x210);
TestSelectorSpecificity("p", 0x010);
TestSelectorSpecificity("div p", 0x020);
TestSelectorSpecificity(".sith", 0x100);
TestSelectorSpecificity("div p.sith", 0x120);
TestSelectorSpecificity("#sith", 0x1000);
TestSelectorSpecificity("body #darkside .sith p", 0x1120);
TestSelectorSpecificity("body #content .data img:hover", 0x1220);
TestSelectorSpecificity("a#a-02", 0x1010);
TestSelectorSpecificity("a[id=\"a-02\"]", 0x0110);
TestSelectorSpecificity("ul#nav li.active a", 0x1130);
TestSelectorSpecificity("body.ie7 .col_3 h2 ~ h2", 0x0230);
TestSelectorSpecificity("#footer *:not(nav) li", 0x1020);
TestSelectorSpecificity("ul > li ul li ol li:first-letter", 0x0070);
}
}
}
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Svg.UnitTests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Svg.UnitTests")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("6ab1d266-f201-46c0-9d14-523768eb18db")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>
</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{E702EB7D-D01D-438A-BADD-E72D4E49109F}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Svg.UnitTests</RootNamespace>
<AssemblyName>Svg.UnitTests</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="System" />
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Drawing" />
<Reference Include="System.Web" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
<Visible>False</Visible>
</CodeAnalysisDependentAssemblyPaths>
</ItemGroup>
<ItemGroup>
<Compile Include="CssQueryTest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Source\Svg.csproj">
<Project>{886A98C5-37C0-4E8B-885E-30C1D2F98B47}</Project>
<Name>Svg</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace SvgW3CTestRunner
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new View());
}
}
}
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("SvgW3CTestRunner")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("SvgW3CTestRunner")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("bf75ea0d-e099-432c-bad5-7fd6cf53d5ee")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.18444
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace SvgW3CTestRunner.Properties
{
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SvgW3CTestRunner.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment