using System;
using System.IO;
using System.Collections.Generic;
using System.Globalization;
using System.Xml;
using System.Linq;
using UnityEngine;
using UnityEngine.Networking;
namespace ToolBuddy.ThirdParty.VectorGraphics
{
/// An enum describing the viewport options to use when importing the SVG document.
public enum ViewportOptions
{
/// Don't preserve the viewport defined in the SVG document.
DontPreserve,
/// Preserves the viewport defined in the SVG document.
PreserveViewport,
/// Applies the root view-box defined in the SVG document (if any).
///
/// This option will rescale the SVG asset to a unit size if a view-box is defined in the SVG document.
/// If no view-box is defined, this option will have the same behavior as `DontPreserve`.
/// It has limited use and is only available for legacy reasons.
///
OnlyApplyRootViewBox
}
/// Reads an SVG document and builds a vector scene.
public class SVGParser
{
/// A structure containing the SVG scene data.
public struct SceneInfo
{
internal SceneInfo(Scene scene, Rect sceneViewport, Dictionary nodeOpacities, Dictionary nodeIDs)
{
Scene = scene;
SceneViewport = sceneViewport;
NodeOpacity = nodeOpacities;
NodeIDs = nodeIDs;
}
/// The vector scene.
public Scene Scene { get; }
/// The position and size of the SVG document
public Rect SceneViewport { get; }
/// A dictionary containing the opacity of the scene nodes.
public Dictionary NodeOpacity { get; }
/// A dictionary containing the scene node for a given ID
public Dictionary NodeIDs { get; }
}
/// Kicks off an SVG file import.
/// The reader object containing the SVG file data
/// The DPI of the SVG file, or 0 to use the device's DPI
/// How many SVG units fit in a Unity unit
/// The default with of the viewport, may be 0
/// The default height of the viewport, may be 0
/// Whether the vector scene should be clipped by the SVG document's viewport
/// A SceneInfo object containing the scene data
public static SceneInfo ImportSVG(TextReader textReader, float dpi = 0.0f, float pixelsPerUnit = 1.0f, int windowWidth = 0, int windowHeight = 0, bool clipViewport = false)
{
var viewportOptions = clipViewport ? ViewportOptions.PreserveViewport : ViewportOptions.DontPreserve;
return ImportSVG(textReader, viewportOptions, dpi, pixelsPerUnit, windowWidth, windowHeight);
}
/// Kicks off an SVG file import.
/// The reader object containing the SVG file data
/// The viewport options to use
/// The DPI of the SVG file, or 0 to use the device's DPI
/// How many SVG units fit in a Unity unit
/// The default with of the viewport, may be 0
/// The default height of the viewport, may be 0
/// A SceneInfo object containing the scene data
public static SceneInfo ImportSVG(TextReader textReader, ViewportOptions viewportOptions, float dpi = 0.0f, float pixelsPerUnit = 1.0f, int windowWidth = 0, int windowHeight = 0)
{
var scene = new Scene();
var settings = new XmlReaderSettings();
settings.IgnoreComments = true;
settings.IgnoreProcessingInstructions = true;
settings.IgnoreWhitespace = true;
// Validation and resolving can reach through HTTP to fetch and validate against schemas/DTDs, which could take ages
#if (NET_STANDARD_2_0 || NET_4_6)
settings.DtdProcessing = System.Xml.DtdProcessing.Ignore;
#else
settings.ProhibitDtd = false;
#endif
settings.ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.None;
settings.ValidationType = ValidationType.None;
settings.XmlResolver = null;
if (dpi == 0.0f)
dpi = Screen.dpi;
Dictionary nodeOpacities;
Dictionary nodeIDs;
SVGDocument doc;
using (var reader = XmlReader.Create(textReader, settings))
{
bool applyRootViewBox =
(viewportOptions == ViewportOptions.PreserveViewport) ||
(viewportOptions == ViewportOptions.OnlyApplyRootViewBox);
doc = new SVGDocument(reader, dpi, scene, windowWidth, windowHeight, applyRootViewBox);
doc.Import();
nodeOpacities = doc.NodeOpacities;
nodeIDs = doc.NodeIDs;
}
float scale = 1.0f / pixelsPerUnit;
if ((scale != 1.0f) && (scene != null) && (scene.Root != null))
scene.Root.Transform = scene.Root.Transform * Matrix2D.Scale(new Vector2(scale, scale));
if ((viewportOptions == ViewportOptions.PreserveViewport) && (scene != null) && (scene.Root != null))
{
// Only add clipper if the scene isn't entirely contained in the viewport
var sceneBounds = VectorUtils.SceneNodeBounds(scene.Root);
if (!doc.sceneViewport.Contains(sceneBounds.min) || !doc.sceneViewport.Contains(sceneBounds.max))
{
var rectClip = new Shape();
VectorUtils.MakeRectangleShape(rectClip, doc.sceneViewport);
// We cannot add the clipper directly on scene.Root since it may have a viewbox transform applied.
// The simplest is to replace the root node with the new "clipped" one, then the clipping
// rectangle can stay in the viewport space (no need to take the viewbox transform into account).
scene.Root = new SceneNode()
{
Children = new List { scene.Root },
Clipper = new SceneNode() { Shapes = new List() { rectClip } }
};
}
}
return new SceneInfo(scene, doc.sceneViewport, nodeOpacities, nodeIDs);
}
}
internal class XmlReaderIterator
{
internal class Node
{
public Node(XmlReader reader) { this.reader = reader; name = reader.Name; depth = reader.Depth; }
public string Name { get { return name; } }
public string this[string attrib] { get { return reader.GetAttribute(attrib); } }
public SVGPropertySheet GetAttributes()
{
var atts = new SVGPropertySheet();
for (int i = 0; i < reader.AttributeCount; ++i)
{
reader.MoveToAttribute(i);
atts[reader.Name] = reader.Value;
}
reader.MoveToElement();
return atts;
}
public SVGFormatException GetException(string message) { return new SVGFormatException(reader, message); }
public SVGFormatException GetUnsupportedAttribValException(string attrib)
{
return new SVGFormatException(reader, "Value '" + this[attrib] + "' is invalid for attribute '" + attrib + "'");
}
public int Depth { get { return depth; } }
XmlReader reader;
int depth;
string name;
}
public XmlReaderIterator(XmlReader reader) { this.reader = reader; }
public bool GoToRoot(string tagName) { return reader.ReadToFollowing(tagName) && reader.Depth == 0; }
public Node VisitCurrent() { currentElementVisited = true; return new Node(reader); }
public bool IsEmptyElement() { return reader.IsEmptyElement; }
public bool GoToNextChild(Node node)
{
if (!currentElementVisited)
return reader.Depth == node.Depth + 1;
reader.Read();
while ((reader.NodeType != XmlNodeType.None) && (reader.NodeType != XmlNodeType.Element))
reader.Read();
if (reader.NodeType != XmlNodeType.Element)
return false;
currentElementVisited = false;
return reader.Depth == node.Depth + 1;
}
public void SkipCurrentChildTree(Node node)
{
while (GoToNextChild(node))
SkipCurrentChildTree(VisitCurrent());
}
public string ReadTextWithinElement()
{
if (reader.IsEmptyElement)
return "";
var text = "";
while (reader.Read() && reader.NodeType != XmlNodeType.EndElement)
text += reader.Value;
return text;
}
XmlReader reader;
bool currentElementVisited;
}
internal class SVGFormatException : Exception
{
public SVGFormatException() {}
public SVGFormatException(string message) : base(ComposeMessage(null, message)) {}
public SVGFormatException(XmlReader reader, string message) : base(ComposeMessage(reader, message)) {}
public static SVGFormatException StackError { get { return new SVGFormatException("Vector scene construction mismatch"); } }
static string ComposeMessage(XmlReader reader, string message)
{
IXmlLineInfo li = reader as IXmlLineInfo;
if (li != null)
return "SVG Error (line " + li.LineNumber + ", character " + li.LinePosition + "): " + message;
return "SVG Error: " + message;
}
}
internal class SVGDictionary : Dictionary {}
internal class SVGPostponedFills : Dictionary { }
internal class SVGDocument
{
public SVGDocument(XmlReader docReader, float dpi, Scene scene, int windowWidth, int windowHeight, bool applyRootViewBox)
{
allElems = new ElemHandler[]
{ circle, defs, ellipse, g, image, line, linearGradient, path, polygon, polyline, radialGradient, clipPath, pattern, mask, rect, symbol, use, style };
// These elements excluded below should not be immediatelly part of the hierarchy and can only be referenced
elemsToAddToHierarchy = new HashSet(new ElemHandler[]
{ circle, /*defs,*/ ellipse, g, image, line, path, polygon, polyline, rect, /*symbol,*/ svg, use });
this.docReader = new XmlReaderIterator(docReader);
this.scene = scene;
this.dpiScale = dpi / 90.0f; // SVG specs assume 90DPI but this machine might use something else
this.windowWidth = windowWidth;
this.windowHeight = windowHeight;
this.applyRootViewBox = applyRootViewBox;
this.svgObjects[StockBlackNonZeroFillName] = new SolidFill() { Color = new Color(0, 0, 0), Mode = FillMode.NonZero };
this.svgObjects[StockBlackOddEvenFillName] = new SolidFill() { Color = new Color(0, 0, 0), Mode = FillMode.OddEven };
}
public void Import()
{
if (scene == null) throw new ArgumentNullException();
if (!docReader.GoToRoot("svg"))
throw new SVGFormatException("Document doesn't have 'svg' root");
currentContainerSize.Push(new Vector2(windowWidth, windowHeight));
svg();
currentContainerSize.Pop();
if (currentContainerSize.Count > 0)
throw SVGFormatException.StackError;
PostProcess(scene.Root);
RemoveInvisibleNodes();
}
public Dictionary NodeOpacities { get { return nodeOpacity; } }
public Dictionary NodeIDs { get { return nodeIDs; } }
internal const float SVGLengthFactor = 1.41421356f; // Used when calculating relative lengths. See http://www.w3.org/TR/SVG/coords.html#Units
static internal string StockBlackNonZeroFillName { get { return "unity_internal_black_nz"; } }
static internal string StockBlackOddEvenFillName { get { return "unity_internal_black_oe"; } }
void ParseChildren(XmlReaderIterator.Node node, string nodeName)
{
var sceneNode = currentSceneNode.Peek();
var supportedChildren = subTags[nodeName];
while (docReader.GoToNextChild(node))
{
var child = docReader.VisitCurrent();
ElemHandler handler;
if (!supportedChildren.TryGetValue(child.Name, out handler))
{
System.Diagnostics.Debug.WriteLine("Skipping over unsupported child (" + child.Name + ") of a (" + node.Name + ")");
docReader.SkipCurrentChildTree(child);
continue;
}
bool addToSceneHierarchy = elemsToAddToHierarchy.Contains(handler);
SceneNode childVectorNode = null;
if (addToSceneHierarchy)
{
if (sceneNode.Children == null)
sceneNode.Children = new List();
childVectorNode = new SceneNode();
nodeGlobalSceneState[childVectorNode] = new NodeGlobalSceneState() { ContainerSize = currentContainerSize.Peek() };
sceneNode.Children.Add(childVectorNode);
currentSceneNode.Push(childVectorNode);
}
styles.PushNode(child);
if (childVectorNode != null)
{
styles.SaveLayerForSceneNode(childVectorNode);
if (styles.Evaluate("display") == "none")
invisibleNodes.Add(new NodeWithParent() { node = childVectorNode, parent = sceneNode });
}
handler();
ParseChildren(child, child.Name); // Recurse
styles.PopNode();
if (addToSceneHierarchy && currentSceneNode.Pop() != childVectorNode)
throw SVGFormatException.StackError;
}
}
#region Tag handling
void circle()
{
var node = docReader.VisitCurrent();
var sceneNode = currentSceneNode.Peek();
ParseID(node, sceneNode);
ParseOpacity(sceneNode);
sceneNode.Transform = SVGAttribParser.ParseTransform(node);
var fill = SVGAttribParser.ParseFill(node, svgObjects, postponedFills, styles);
PathCorner strokeCorner;
PathEnding strokeEnding;
var stroke = ParseStrokeAttributeSet(node, out strokeCorner, out strokeEnding);
float cx = AttribLengthVal(node, "cx", 0.0f, DimType.Width);
float cy = AttribLengthVal(node, "cy", 0.0f, DimType.Height);
float r = AttribLengthVal(node, "r", 0.0f, DimType.Length);
var circle = new Shape();
VectorUtils.MakeCircleShape(circle, new Vector2(cx, cy), r);
circle.PathProps = new PathProperties() { Stroke = stroke, Head = strokeEnding, Tail = strokeEnding, Corners = strokeCorner };
circle.Fill = fill;
sceneNode.Shapes = new List(1);
sceneNode.Shapes.Add(circle);
ParseClipAndMask(node, sceneNode);
AddToSVGDictionaryIfPossible(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node); // No children supported
}
void defs()
{
var node = docReader.VisitCurrent();
var sceneNode = new SceneNode(); // A new scene node instead of one precreated for us
ParseOpacity(sceneNode);
sceneNode.Transform = SVGAttribParser.ParseTransform(node);
AddToSVGDictionaryIfPossible(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node, allElems);
currentSceneNode.Push(sceneNode);
ParseChildren(node, node.Name);
if (currentSceneNode.Pop() != sceneNode)
throw SVGFormatException.StackError;
}
void ellipse()
{
var node = docReader.VisitCurrent();
var sceneNode = currentSceneNode.Peek();
ParseID(node, sceneNode);
ParseOpacity(sceneNode);
sceneNode.Transform = SVGAttribParser.ParseTransform(node);
var fill = SVGAttribParser.ParseFill(node, svgObjects, postponedFills, styles);
PathCorner strokeCorner;
PathEnding strokeEnding;
var stroke = ParseStrokeAttributeSet(node, out strokeCorner, out strokeEnding);
float cx = AttribLengthVal(node, "cx", 0.0f, DimType.Width);
float cy = AttribLengthVal(node, "cy", 0.0f, DimType.Height);
float rx = AttribLengthVal(node, "rx", 0.0f, DimType.Length);
float ry = AttribLengthVal(node, "ry", 0.0f, DimType.Length);
var ellipse = new Shape();
VectorUtils.MakeEllipseShape(ellipse, new Vector2(cx, cy), rx, ry);
ellipse.PathProps = new PathProperties() { Stroke = stroke, Corners = strokeCorner, Head = strokeEnding, Tail = strokeEnding };
ellipse.Fill = fill;
sceneNode.Shapes = new List(1);
sceneNode.Shapes.Add(ellipse);
ParseClipAndMask(node, sceneNode);
AddToSVGDictionaryIfPossible(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node); // No children supported
}
void g()
{
var node = docReader.VisitCurrent();
var sceneNode = currentSceneNode.Peek();
ParseID(node, sceneNode);
ParseOpacity(sceneNode);
sceneNode.Transform = SVGAttribParser.ParseTransform(node);
ParseClipAndMask(node, sceneNode);
AddToSVGDictionaryIfPossible(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node, allElems);
}
void image()
{
var node = docReader.VisitCurrent();
var sceneNode = currentSceneNode.Peek();
// Try to get the referenced image first, if we fail, we just ignore the whole thing
var url = node["xlink:href"];
if (url != null)
{
var textureFill = new TextureFill();
textureFill.Mode = FillMode.NonZero;
textureFill.Addressing = AddressMode.Clamp;
var lowercaseURL = url.ToLower();
if (lowercaseURL.StartsWith("data:"))
{
textureFill.Texture = DecodeTextureData(url);
}
else
{
if (!lowercaseURL.Contains("://"))
{
#if UNITY_EDITOR
textureFill.Texture = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/" + url);
#endif
}
else if (lowercaseURL.StartsWith("http://") || lowercaseURL.StartsWith("https://"))
{
#pragma warning disable 618
// WWW is obsolete (replaced with UnityWebRequest), but this is the class that works best
// with editor code. We will continue to use WWW until UnityWebRequest works better in an editor
// environment.
using (WWW www = new WWW(url))
{
while (www.keepWaiting)
System.Threading.Thread.Sleep(10); // Progress bar please...
textureFill.Texture = www.texture;
}
#pragma warning restore 618
}
else
{
Debug.LogWarning("Unsupported URL scheme for (only http/https is supported): " + url);
}
}
if (textureFill.Texture != null)
{
// Fills and strokes don't seem to apply to image despite what the specs say
// All browsers and editing tools seem to ignore them, so we'll just do as well
ParseID(node, sceneNode);
ParseOpacity(sceneNode);
sceneNode.Transform = SVGAttribParser.ParseTransform(node);
var viewPort = ParseViewport(node, sceneNode, currentContainerSize.Peek());
sceneNode.Transform = sceneNode.Transform * Matrix2D.Translate(viewPort.position);
var viewBoxInfo = new ViewBoxInfo();
viewBoxInfo.ViewBox = new Rect(0, 0, textureFill.Texture.width, textureFill.Texture.height);
ParseViewBoxAspectRatio(node, ref viewBoxInfo);
ApplyViewBox(sceneNode, viewBoxInfo, viewPort);
var rect = new Shape();
VectorUtils.MakeRectangleShape(rect, new Rect(0, 0, textureFill.Texture.width, textureFill.Texture.height));
rect.Fill = textureFill;
sceneNode.Shapes = new List(1);
sceneNode.Shapes.Add(rect);
ParseClipAndMask(node, sceneNode);
}
}
// Resolve any previous node that was referencing this image
string id = node["id"];
if (!string.IsNullOrEmpty(id))
{
List refList;
if (postponedSymbolData.TryGetValue(id, out refList))
{
foreach (var refData in refList)
ResolveReferencedNode(sceneNode, refData, true);
}
}
AddToSVGDictionaryIfPossible(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node); // No children supported
}
void line()
{
var node = docReader.VisitCurrent();
var sceneNode = currentSceneNode.Peek();
ParseID(node, sceneNode);
ParseOpacity(sceneNode);
sceneNode.Transform = SVGAttribParser.ParseTransform(node);
PathCorner strokeCorner;
PathEnding strokeEnding;
var stroke = ParseStrokeAttributeSet(node, out strokeCorner, out strokeEnding);
float x1 = AttribLengthVal(node, "x1", 0.0f, DimType.Width);
float y1 = AttribLengthVal(node, "y1", 0.0f, DimType.Height);
float x2 = AttribLengthVal(node, "x2", 0.0f, DimType.Width);
float y2 = AttribLengthVal(node, "y2", 0.0f, DimType.Height);
var path = new Shape();
path.PathProps = new PathProperties() { Stroke = stroke, Head = strokeEnding, Tail = strokeEnding };
path.Contours = new BezierContour[] {
new BezierContour() { Segments = VectorUtils.BezierSegmentToPath(VectorUtils.MakeLine(new Vector2(x1, y1), new Vector2(x2, y2))) }
};
sceneNode.Shapes = new List(1);
sceneNode.Shapes.Add(path);
ParseClipAndMask(node, sceneNode);
AddToSVGDictionaryIfPossible(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node); // No children supported
}
void linearGradient()
{
var node = docReader.VisitCurrent();
var link = node["xlink:href"];
var refFill = SVGAttribParser.ParseRelativeRef(link, svgObjects) as GradientFill;
var refFillData = refFill != null ? gradientExInfo[refFill] as LinearGradientExData : null;
bool relativeToWorld = refFillData != null ? refFillData.WorldRelative : false;
switch (node["gradientUnits"])
{
case null:
break;
case "objectBoundingBox":
relativeToWorld = false;
break;
case "userSpaceOnUse":
relativeToWorld = true;
break;
default:
throw node.GetUnsupportedAttribValException("gradientUnits");
}
AddressMode addressing = refFill != null ? refFill.Addressing : AddressMode.Clamp;
switch (node["spreadMethod"])
{
case null:
break;
case "pad":
addressing = AddressMode.Clamp;
break;
case "reflect":
addressing = AddressMode.Mirror;
break;
case "repeat":
addressing = AddressMode.Wrap;
break;
default:
throw node.GetUnsupportedAttribValException("spreadMethod");
}
var gradientTransform = SVGAttribParser.ParseTransform(node, "gradientTransform");
GradientFill fill = CloneGradientFill(refFill);
if (fill == null)
fill = new GradientFill() { Addressing = addressing, Type = GradientFillType.Linear };
LinearGradientExData fillExData = new LinearGradientExData() { WorldRelative = relativeToWorld, FillTransform = gradientTransform };
gradientExInfo[fill] = fillExData;
// Fills are defined outside of a shape scope, so we can't resolve relative coordinates here.
// We defer this entire operation to AdjustFills pass, but we still do value validation here
// nonetheless to give meaningful error messages to the user if any.
currentContainerSize.Push(Vector2.one);
fillExData.X1 = node["x1"];
fillExData.Y1 = node["y1"];
fillExData.X2 = node["x2"];
fillExData.Y2 = node["y2"];
// The calls below are ineffective but they validate the inputs and throw an error if wrong values are specified, so don't remove them
AttribLengthVal(fillExData.X1, node, "x1", 0.0f, DimType.Width);
AttribLengthVal(fillExData.Y1, node, "y1", 0.0f, DimType.Height);
AttribLengthVal(fillExData.X2, node, "x2", 1.0f, DimType.Width);
AttribLengthVal(fillExData.Y2, node, "y2", 0.0f, DimType.Height);
currentContainerSize.Pop();
currentGradientFill = fill; // Children stops will register to this fill now
currentGradientId = node["id"];
currentGradientLink = SVGAttribParser.CleanIri(link);
if (!string.IsNullOrEmpty(link) && !svgObjects.ContainsKey(link))
{
// Reference may be defined later in the file. Save for postponed processing.
if (!postponedStopData.ContainsKey(currentGradientLink))
postponedStopData.Add(currentGradientLink, new List());
postponedStopData[currentGradientLink].Add(new PostponedStopData() { fill = fill });
}
AddToSVGDictionaryIfPossible(node, fill);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node, stop);
}
void path()
{
var node = docReader.VisitCurrent();
var sceneNode = currentSceneNode.Peek();
ParseID(node, sceneNode);
ParseOpacity(sceneNode);
sceneNode.Transform = SVGAttribParser.ParseTransform(node);
var fill = SVGAttribParser.ParseFill(node, svgObjects, postponedFills, styles);
PathCorner strokeCorner;
PathEnding strokeEnding;
var stroke = ParseStrokeAttributeSet(node, out strokeCorner, out strokeEnding);
var pathProps = new PathProperties() { Stroke = stroke, Corners = strokeCorner, Head = strokeEnding, Tail = strokeEnding };
// A path may have 1 or more sub paths. Each for us is an individual vector path.
var contours = SVGAttribParser.ParsePath(node);
if ((contours != null) && (contours.Count > 0))
{
//float pathLength = AttribFloatVal(node, "pathLength"); // This is useful for animation purposes mostly
sceneNode.Shapes = new List(1);
sceneNode.Shapes.Add(new Shape() { Contours = contours.ToArray(), Fill = fill, PathProps = pathProps });
AddToSVGDictionaryIfPossible(node, sceneNode);
}
ParseClipAndMask(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node); // No children supported
}
void polygon()
{
var node = docReader.VisitCurrent();
var sceneNode = currentSceneNode.Peek();
ParseID(node, sceneNode);
ParseOpacity(sceneNode);
sceneNode.Transform = SVGAttribParser.ParseTransform(node);
var fill = SVGAttribParser.ParseFill(node, svgObjects, postponedFills, styles);
PathCorner strokeCorner;
PathEnding strokeEnding;
var stroke = ParseStrokeAttributeSet(node, out strokeCorner, out strokeEnding);
var pointsAttribVal = node["points"];
var pointsString = (pointsAttribVal != null) ? pointsAttribVal.Split(whiteSpaceNumberChars, StringSplitOptions.RemoveEmptyEntries) : null;
if (pointsString != null)
{
if ((pointsString.Length & 1) == 1)
throw node.GetException("polygon 'points' must specify x,y for each coordinate");
if (pointsString.Length < 4)
throw node.GetException("polygon 'points' do not even specify one triangle");
var pathProps = new PathProperties() { Stroke = stroke, Corners = strokeCorner, Head = strokeEnding, Tail = strokeEnding };
var contour = new BezierContour() { Closed = true };
var lastPoint = new Vector2(
AttribLengthVal(pointsString[0], node, "points", 0.0f, DimType.Width),
AttribLengthVal(pointsString[1], node, "points", 0.0f, DimType.Height));
int maxSegments = pointsString.Length / 2;
var segments = new List(maxSegments);
for (int i = 1; i < maxSegments; i++)
{
var newPoint = new Vector2(
AttribLengthVal(pointsString[i * 2 + 0], node, "points", 0.0f, DimType.Width),
AttribLengthVal(pointsString[i * 2 + 1], node, "points", 0.0f, DimType.Height));
if (newPoint == lastPoint)
continue;
var seg = VectorUtils.MakeLine(lastPoint, newPoint);
segments.Add(new BezierPathSegment() { P0 = seg.P0, P1 = seg.P1, P2 = seg.P2 });
lastPoint = newPoint;
}
if (segments.Count > 0)
{
var connect = VectorUtils.MakeLine(lastPoint, segments[0].P0);
segments.Add(new BezierPathSegment() { P0 = connect.P0, P1 = connect.P1, P2 = connect.P2 });
contour.Segments = segments.ToArray();
var shape = new Shape() { Contours = new BezierContour[] { contour }, PathProps = pathProps, Fill = fill };
sceneNode.Shapes = new List(1);
sceneNode.Shapes.Add(shape);
}
}
ParseClipAndMask(node, sceneNode);
AddToSVGDictionaryIfPossible(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node); // No children supported
}
void polyline()
{
var node = docReader.VisitCurrent();
var sceneNode = currentSceneNode.Peek();
ParseID(node, sceneNode);
ParseOpacity(sceneNode);
sceneNode.Transform = SVGAttribParser.ParseTransform(node);
var fill = SVGAttribParser.ParseFill(node, svgObjects, postponedFills, styles);
PathCorner strokeCorner;
PathEnding strokeEnding;
var stroke = ParseStrokeAttributeSet(node, out strokeCorner, out strokeEnding);
var pointsAttribVal = node["points"];
var pointsString = (pointsAttribVal != null) ? pointsAttribVal.Split(whiteSpaceNumberChars, StringSplitOptions.RemoveEmptyEntries) : null;
if (pointsString != null)
{
if ((pointsString.Length & 1) == 1)
throw node.GetException("polyline 'points' must specify x,y for each coordinate");
if (pointsString.Length < 4)
throw node.GetException("polyline 'points' do not even specify one line");
var shape = new Shape() { Fill = fill };
shape.PathProps = new PathProperties() { Stroke = stroke, Corners = strokeCorner, Head = strokeEnding, Tail = strokeEnding };
var lastPoint = new Vector2(
AttribLengthVal(pointsString[0], node, "points", 0.0f, DimType.Width),
AttribLengthVal(pointsString[1], node, "points", 0.0f, DimType.Height));
int maxSegments = pointsString.Length / 2;
var segments = new List(maxSegments);
for (int i = 1; i < maxSegments; i++)
{
var newPoint = new Vector2(
AttribLengthVal(pointsString[i * 2 + 0], node, "points", 0.0f, DimType.Width),
AttribLengthVal(pointsString[i * 2 + 1], node, "points", 0.0f, DimType.Height));
if (newPoint == lastPoint)
continue;
var seg = VectorUtils.MakeLine(lastPoint, newPoint);
segments.Add(new BezierPathSegment() { P0 = seg.P0, P1 = seg.P1, P2 = seg.P2 });
lastPoint = newPoint;
}
if (segments.Count > 0 )
{
var connect = VectorUtils.MakeLine(lastPoint, segments[0].P0);
segments.Add(new BezierPathSegment() { P0 = connect.P0, P1 = connect.P1, P2 = connect.P2 });
shape.Contours = new BezierContour[] {
new BezierContour() { Segments = segments.ToArray() }
};
sceneNode.Shapes = new List(1);
sceneNode.Shapes.Add(shape);
}
}
ParseClipAndMask(node, sceneNode);
AddToSVGDictionaryIfPossible(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node); // No children supported
}
void radialGradient()
{
var node = docReader.VisitCurrent();
var link = node["xlink:href"];
var refFill = SVGAttribParser.ParseRelativeRef(link, svgObjects) as GradientFill;
var refFillData = refFill != null ? gradientExInfo[refFill] as RadialGradientExData : null;
bool relativeToWorld = refFillData != null ? refFillData.WorldRelative : false;
switch (node["gradientUnits"])
{
case null:
break;
case "objectBoundingBox":
relativeToWorld = false;
break;
case "userSpaceOnUse":
relativeToWorld = true;
break;
default:
throw node.GetUnsupportedAttribValException("gradientUnits");
}
AddressMode addressing = refFill != null ? refFill.Addressing : AddressMode.Clamp;
switch (node["spreadMethod"])
{
case null:
break;
case "pad":
addressing = AddressMode.Clamp;
break;
case "reflect":
addressing = AddressMode.Mirror;
break;
case "repeat":
addressing = AddressMode.Wrap;
break;
default:
throw node.GetUnsupportedAttribValException("spreadMethod");
}
var gradientTransform = SVGAttribParser.ParseTransform(node, "gradientTransform");
GradientFill fill = CloneGradientFill(refFill);
if (fill == null)
fill = new GradientFill() { Addressing = addressing, Type = GradientFillType.Radial };
RadialGradientExData fillExData = new RadialGradientExData() { WorldRelative = relativeToWorld, FillTransform = gradientTransform };
gradientExInfo[fill] = fillExData;
// Fills are defined outside of a shape scope, so we can't resolve relative coordinates here.
// We defer this entire operation to AdjustFills pass, but we still do value validation here
// nonetheless to give meaningful error messages to the user if any.
currentContainerSize.Push(Vector2.one);
fillExData.Cx = node["cx"];
fillExData.Cy = node["cy"];
fillExData.Fx = node["fx"];
fillExData.Fy = node["fy"];
fillExData.R = node["r"];
// The calls below are ineffective but they validate the inputs and throw an error if wrong values are specified, so don't remove them
AttribLengthVal(fillExData.Cx, node, "cx", 0.5f, DimType.Width);
AttribLengthVal(fillExData.Cy, node, "cy", 0.5f, DimType.Height);
AttribLengthVal(fillExData.Fx, node, "fx", 0.5f, DimType.Width);
AttribLengthVal(fillExData.Fy, node, "fy", 0.5f, DimType.Height);
AttribLengthVal(fillExData.R, node, "r", 0.5f, DimType.Length);
currentContainerSize.Pop();
currentGradientFill = fill; // Children stops will register to this fill now
currentGradientId = node["id"];
currentGradientLink = SVGAttribParser.CleanIri(link);
if (!string.IsNullOrEmpty(link) && !svgObjects.ContainsKey(link))
{
// Reference may be defined later in the file. Save for postponed processing.
if (!postponedStopData.ContainsKey(currentGradientLink))
postponedStopData.Add(currentGradientLink, new List());
postponedStopData[currentGradientLink].Add(new PostponedStopData() { fill = fill });
}
AddToSVGDictionaryIfPossible(node, fill);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node, stop);
}
void clipPath()
{
var node = docReader.VisitCurrent();
string id = node["id"];
// A new scene node instead of one precreated for us
var clipRoot = new SceneNode() {
Transform = SVGAttribParser.ParseTransform(node)
};
bool relativeToWorld;
switch (node["clipPathUnits"])
{
case null:
case "userSpaceOnUse":
relativeToWorld = true;
break;
case "objectBoundingBox":
relativeToWorld = false;
break;
default:
throw node.GetUnsupportedAttribValException("clipPathUnits");
}
clipData[clipRoot] = new ClipData() { WorldRelative = relativeToWorld };
AddToSVGDictionaryIfPossible(node, clipRoot);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node, allElems);
currentSceneNode.Push(clipRoot);
ParseChildren(node, node.Name);
if (currentSceneNode.Pop() != clipRoot)
throw SVGFormatException.StackError;
// Resolve any previous node that was referencing this clipping path
if (!string.IsNullOrEmpty(id))
{
List clips;
if (postponedClip.TryGetValue(id, out clips))
{
foreach (var clip in clips)
ApplyClipper(clipRoot, clip.node, relativeToWorld);
}
}
}
void pattern()
{
var node = docReader.VisitCurrent();
// A new scene node instead of one precreated for us
var patternRoot = new SceneNode() {
Transform = Matrix2D.identity
};
bool relativeToWorld = false;
switch (node["patternUnits"])
{
case null:
case "objectBoundingBox":
relativeToWorld = false;
break;
case "userSpaceOnUse":
relativeToWorld = true;
break;
default:
throw node.GetUnsupportedAttribValException("patternUnits");
}
bool contentRelativeToWorld = true;
switch (node["patternContentUnits"])
{
case null:
case "userSpaceOnUse":
contentRelativeToWorld = true;
break;
case "objectBoundingBox":
contentRelativeToWorld = false;
break;
default:
throw node.GetUnsupportedAttribValException("patternContentUnits");
}
var x = AttribLengthVal(node["x"], node, "x", 0.0f, DimType.Width);
var y = AttribLengthVal(node["y"], node, "y", 0.0f, DimType.Height);
var w = AttribLengthVal(node["width"], node, "width", 0.0f, DimType.Width);
var h = AttribLengthVal(node["height"], node, "height", 0.0f, DimType.Height);
var patternTransform = SVGAttribParser.ParseTransform(node, "patternTransform");
patternData[patternRoot] = new PatternData() {
WorldRelative = relativeToWorld,
ContentWorldRelative = contentRelativeToWorld,
PatternTransform = patternTransform
};
var fill = new PatternFill() {
Pattern = patternRoot,
Rect = new Rect(x, y, w, h)
};
AddToSVGDictionaryIfPossible(node, fill);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node, allElems);
currentSceneNode.Push(patternRoot);
ParseChildren(node, node.Name);
if (currentSceneNode.Pop() != patternRoot)
throw SVGFormatException.StackError;
}
void mask()
{
var node = docReader.VisitCurrent();
// A new scene node instead of one precreated for us
var maskRoot = new SceneNode() {
Transform = Matrix2D.identity
};
bool relativeToWorld;
switch (node["maskUnits"])
{
case null:
case "userSpaceOnUse":
relativeToWorld = true;
break;
case "objectBoundingBox":
relativeToWorld = false;
break;
default:
throw node.GetUnsupportedAttribValException("maskUnits");
}
bool contentRelativeToWorld;
switch (node["maskContentUnits"])
{
case null:
case "userSpaceOnUse":
contentRelativeToWorld = true;
break;
case "objectBoundingBox":
contentRelativeToWorld = false;
break;
default:
throw node.GetUnsupportedAttribValException("maskContentUnits");
}
maskData[maskRoot] = new MaskData() {
WorldRelative = relativeToWorld,
ContentWorldRelative = contentRelativeToWorld,
};
AddToSVGDictionaryIfPossible(node, maskRoot);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node, allElems);
currentSceneNode.Push(maskRoot);
ParseChildren(node, node.Name);
if (currentSceneNode.Pop() != maskRoot)
throw SVGFormatException.StackError;
}
void rect()
{
var node = docReader.VisitCurrent();
var sceneNode = currentSceneNode.Peek();
ParseID(node, sceneNode);
ParseOpacity(sceneNode);
sceneNode.Transform = SVGAttribParser.ParseTransform(node);
var fill = SVGAttribParser.ParseFill(node, svgObjects, postponedFills, styles);
PathCorner strokeCorner;
PathEnding strokeEnding;
var stroke = ParseStrokeAttributeSet(node, out strokeCorner, out strokeEnding);
float x = AttribLengthVal(node, "x", 0.0f, DimType.Width);
float y = AttribLengthVal(node, "y", 0.0f, DimType.Height);
float rx = AttribLengthVal(node, "rx", -1.0f, DimType.Length);
float ry = AttribLengthVal(node, "ry", -1.0f, DimType.Length);
float width = AttribLengthVal(node, "width", 0.0f, DimType.Length);
float height = AttribLengthVal(node, "height", 0.0f, DimType.Length);
if ((rx < 0.0f) && (ry >= 0.0f))
rx = ry;
else if ((ry < 0.0f) && (rx >= 0.0f))
ry = rx;
else if ((ry < 0.0f) && (rx < 0.0f))
rx = ry = 0.0f;
rx = Mathf.Min(rx, width * 0.5f);
ry = Mathf.Min(ry, height * 0.5f);
var rad = new Vector2(rx, ry);
var rect = new Shape();
VectorUtils.MakeRectangleShape(rect, new Rect(x, y, width, height), rad, rad, rad, rad);
rect.Fill = fill;
rect.PathProps = new PathProperties() { Stroke = stroke, Head = strokeEnding, Tail = strokeEnding, Corners = strokeCorner };
sceneNode.Shapes = new List(1);
sceneNode.Shapes.Add(rect);
ParseClipAndMask(node, sceneNode);
AddToSVGDictionaryIfPossible(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node); // No children supported
}
void stop()
{
var node = docReader.VisitCurrent();
System.Diagnostics.Debug.Assert(currentGradientFill != null);
GradientStop stop = new GradientStop();
string stopColor = styles.Evaluate("stop-color");
Color color = stopColor != null ? SVGAttribParser.ParseColor(stopColor) : Color.black;
color.a = AttribFloatVal("stop-opacity", 1.0f);
stop.Color = color;
string offsetString = styles.Evaluate("offset");
if (!string.IsNullOrEmpty(offsetString))
{
bool percentage = offsetString.EndsWith("%");
if (percentage)
offsetString = offsetString.Substring(0, offsetString.Length - 1);
stop.StopPercentage = SVGAttribParser.ParseFloat(offsetString);
if (percentage)
stop.StopPercentage /= 100.0f;
stop.StopPercentage = Mathf.Max(0.0f, stop.StopPercentage);
stop.StopPercentage = Mathf.Min(1.0f, stop.StopPercentage);
}
// I don't like this, but hopefully there aren't many stops in a gradient
GradientStop[] newStops;
if (currentGradientFill.Stops == null || currentGradientFill.Stops.Length == 0)
newStops = new GradientStop[1];
else
{
newStops = new GradientStop[currentGradientFill.Stops.Length + 1];
currentGradientFill.Stops.CopyTo(newStops, 0);
}
newStops[newStops.Length - 1] = stop;
currentGradientFill.Stops = newStops;
// Apply postponed stops if this was defined later in the file
if (!string.IsNullOrEmpty(currentGradientId) && postponedStopData.ContainsKey(currentGradientId))
{
foreach (var postponedStop in postponedStopData[currentGradientId])
postponedStop.fill.Stops = newStops;
}
// Local stops overrides referenced ones
if (!string.IsNullOrEmpty(currentGradientLink) && postponedStopData.ContainsKey(currentGradientLink))
{
var stopDataList = postponedStopData[currentGradientLink];
foreach (var postponedStop in stopDataList)
{
if (postponedStop.fill == currentGradientFill)
{
stopDataList.Remove(postponedStop);
break;
}
}
}
if (ShouldDeclareSupportedChildren(node))
SupportElems(node); // No children supported
}
void svg()
{
var node = docReader.VisitCurrent();
var sceneNode = new SceneNode();
if (scene.Root == null) // If this is the root SVG element, then we set the vector scene root as well
{
System.Diagnostics.Debug.Assert(currentSceneNode.Count == 0);
scene.Root = sceneNode;
}
styles.PushNode(node);
ParseID(node, sceneNode);
ParseOpacity(sceneNode);
sceneViewport = ParseViewport(node, sceneNode, new Vector2(windowWidth, windowHeight));
var viewBoxInfo = ParseViewBox(node, sceneNode, sceneViewport);
if (applyRootViewBox)
ApplyViewBox(sceneNode, viewBoxInfo, sceneViewport);
currentContainerSize.Push(sceneViewport.size);
if (!viewBoxInfo.IsEmpty)
currentViewBoxSize.Push(viewBoxInfo.ViewBox.size);
currentSceneNode.Push(sceneNode);
nodeGlobalSceneState[sceneNode] = new NodeGlobalSceneState() { ContainerSize = currentContainerSize.Peek() };
if (ShouldDeclareSupportedChildren(node))
SupportElems(node, allElems);
ParseChildren(node, "svg");
if (currentSceneNode.Pop() != sceneNode)
throw SVGFormatException.StackError;
if (!viewBoxInfo.IsEmpty)
currentViewBoxSize.Pop();
currentContainerSize.Pop();
styles.PopNode();
}
void symbol()
{
var node = docReader.VisitCurrent();
var sceneNode = new SceneNode(); // A new scene node instead of one precreated for us
string id = node["id"];
ParseID(node, sceneNode);
ParseOpacity(sceneNode);
sceneNode.Transform = Matrix2D.identity;
Rect viewportRect = new Rect(Vector2.zero, currentContainerSize.Peek());
var viewBoxInfo = ParseViewBox(node, sceneNode, viewportRect);
if (!viewBoxInfo.IsEmpty)
currentViewBoxSize.Push(viewBoxInfo.ViewBox.size);
symbolViewBoxes[sceneNode] = viewBoxInfo;
AddToSVGDictionaryIfPossible(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node, allElems);
currentSceneNode.Push(sceneNode);
ParseChildren(node, node.Name);
if (currentSceneNode.Pop() != sceneNode)
throw SVGFormatException.StackError;
if (!viewBoxInfo.IsEmpty)
currentViewBoxSize.Pop();
ParseClipAndMask(node, sceneNode);
// Resolve any previous node that was referencing this symbol
if (!string.IsNullOrEmpty(id))
{
List refList;
if (postponedSymbolData.TryGetValue(id, out refList))
{
foreach (var refData in refList)
ResolveReferencedNode(sceneNode, refData, true);
}
}
}
void use()
{
var node = docReader.VisitCurrent();
var sceneNode = currentSceneNode.Peek();
ParseOpacity(sceneNode);
var sceneViewport = ParseViewport(node, sceneNode, Vector2.zero);
var refData = new NodeReferenceData() {
node = sceneNode,
viewport = sceneViewport,
id = node["id"]
};
var iri = node["xlink:href"];
var referencedNode = SVGAttribParser.ParseRelativeRef(iri, svgObjects) as SceneNode;
if (referencedNode == null && !string.IsNullOrEmpty(iri) && iri.StartsWith("#"))
{
// The referenced node may be defined later in the file, save it for later
iri = iri.Substring(1);
List refList;
if (!postponedSymbolData.TryGetValue(iri, out refList))
{
refList = new List();
postponedSymbolData[iri] = refList;
}
refList.Add(refData);
}
sceneNode.Transform = SVGAttribParser.ParseTransform(node);
sceneNode.Transform = sceneNode.Transform * Matrix2D.Translate(sceneViewport.position);
if (referencedNode != null)
ResolveReferencedNode(referencedNode, refData, false);
ParseClipAndMask(node, sceneNode);
AddToSVGDictionaryIfPossible(node, sceneNode);
if (ShouldDeclareSupportedChildren(node))
SupportElems(node); // No children supported
}
void style()
{
var node = docReader.VisitCurrent();
var text = docReader.ReadTextWithinElement();
if (text.Length > 0)
styles.SetGlobalStyleSheet(SVGStyleSheetUtils.Parse(text));
if (ShouldDeclareSupportedChildren(node))
SupportElems(node); // No children supported
}
#endregion
#region Symbol Reference Processing
private void ResolveReferencedNode(SceneNode referencedNode, NodeReferenceData refData, bool isDeferred)
{
// Note we don't use the viewport size because the