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 element doesn't establish a viewport for its referenced elements ViewBoxInfo viewBoxInfo; if (symbolViewBoxes.TryGetValue(referencedNode, out viewBoxInfo)) ApplyViewBox(refData.node, viewBoxInfo, refData.viewport); // When using a symbol we need to apply the symbol's view box if (refData.node.Children == null) refData.node.Children = new List(); SVGStyleResolver.StyleLayer rootLayer = null; if (isDeferred) { // If deferred, push back the original tag style layer to be in the same "style environment" rootLayer = styles.GetLayerForScenNode(refData.node); if (rootLayer != null) styles.PushLayer(rootLayer); } // Activate the styles of the referenced node var styleLayer = nodeStyleLayers[referencedNode]; if (styleLayer != null) styles.PushLayer(styleLayer); // Build a map to be able to retrieve the original node's style layer var originalNodes = new List(10); foreach (var child in VectorUtils.SceneNodes(referencedNode)) originalNodes.Add(child); var node = CloneSceneNode(referencedNode); int originalIndex = 0; foreach (var child in VectorUtils.SceneNodes(node)) { var nodeIndex = originalIndex++; if (child.Shapes == null) continue; var originalNode = originalNodes[nodeIndex]; var layer = styles.GetLayerForScenNode(originalNode); if (layer != null) styles.PushLayer(layer); bool isDefaultFill; var fill = SVGAttribParser.ParseFill(null, svgObjects, postponedFills, styles, Inheritance.Inherited, out isDefaultFill); PathCorner strokeCorner; PathEnding strokeEnding; var stroke = ParseStrokeAttributeSet(null, out strokeCorner, out strokeEnding); foreach (var shape in child.Shapes) { var pathProps = shape.PathProps; pathProps.Stroke = stroke; pathProps.Corners = strokeCorner; pathProps.Head = strokeEnding; shape.PathProps = pathProps; shape.Fill = isDefaultFill ? shape.Fill : fill; } if (layer != null) styles.PopLayer(); } if (styleLayer != null) styles.PopLayer(); if (rootLayer != null) styles.PopLayer(); // We process the node ID here to refer to the proper scene node if (!string.IsNullOrEmpty(refData.id)) nodeIDs[refData.id] = node; refData.node.Children.Add(node); } #endregion #region Scene Node Cloning // This is a poor man's cloning system, until we have proper serialization in VectorScene. private SceneNode CloneSceneNode(SceneNode node) { if (node == null) return null; List children = null; if (node.Children != null) { children = new List(node.Children.Count); foreach (var c in node.Children) children.Add(CloneSceneNode(c)); } List shapes = null; if (node.Shapes != null) { shapes = new List(node.Shapes.Count); foreach (var d in node.Shapes) shapes.Add(CloneShape(d)); } var n = new SceneNode() { Children = children, Shapes = shapes, Transform = node.Transform, Clipper = CloneSceneNode(node.Clipper) }; if (nodeGlobalSceneState.ContainsKey(node)) nodeGlobalSceneState[n] = nodeGlobalSceneState[node]; if (nodeOpacity.ContainsKey(node)) nodeOpacity[n] = nodeOpacity[node]; return n; } private Shape CloneShape(Shape shape) { if (shape == null) return null; BezierContour[] contours = null; if (shape.Contours != null) { contours = new BezierContour[shape.Contours.Length]; for (int i = 0; i < contours.Length; ++i) contours[i] = CloneContour(shape.Contours[i]); } return new Shape() { Fill = CloneFill(shape.Fill), FillTransform = shape.FillTransform, PathProps = ClonePathProps(shape.PathProps), Contours = contours, IsConvex = shape.IsConvex }; } private BezierContour CloneContour(BezierContour c) { BezierPathSegment[] segs = null; if (c.Segments != null) { segs = new BezierPathSegment[c.Segments.Length]; for (int i = 0; i < segs.Length; ++i) { var s = c.Segments[i]; segs[i] = new BezierPathSegment() { P0 = s.P0, P1 = s.P1, P2 = s.P2 }; } } return new BezierContour() { Segments = segs, Closed = c.Closed }; } private IFill CloneFill(IFill fill) { if (fill == null) return null; IFill f = null; if (fill is SolidFill) { var solid = fill as SolidFill; f = new SolidFill() { Color = solid.Color, Opacity = solid.Opacity, Mode = solid.Mode }; } else if (fill is GradientFill) { var grad = fill as GradientFill; GradientStop[] stops = null; if (grad.Stops != null) { stops = new GradientStop[grad.Stops.Length]; for (int i = 0; i < stops.Length; ++i) { var stop = grad.Stops[i]; stops[i] = new GradientStop() { Color = stop.Color, StopPercentage = stop.StopPercentage }; } } var gradientFill = new GradientFill() { Type = grad.Type, Stops = stops, Mode = grad.Mode, Opacity = grad.Opacity, Addressing = grad.Addressing, RadialFocus = grad.RadialFocus }; gradientExInfo[gradientFill] = gradientExInfo[grad]; f = gradientFill; } else if (fill is TextureFill) { var tex = fill as TextureFill; f = new TextureFill() { Texture = tex.Texture, Mode = tex.Mode, Opacity = tex.Opacity, Addressing = tex.Addressing }; } else if (fill is PatternFill) { var pat = fill as PatternFill; f = new PatternFill() { Mode = pat.Mode, Opacity = pat.Opacity, Pattern = CloneSceneNode(pat.Pattern), Rect = pat.Rect }; } return f; } private PathProperties ClonePathProps(PathProperties props) { Stroke stroke = null; if (props.Stroke != null) { float[] pattern = null; if (props.Stroke.Pattern != null) { pattern = new float[props.Stroke.Pattern.Length]; for (int i = 0; i < pattern.Length; ++i) pattern[i] = props.Stroke.Pattern[i]; } stroke = new Stroke() { Fill = CloneFill(props.Stroke.Fill), FillTransform = props.Stroke.FillTransform, HalfThickness = props.Stroke.HalfThickness, Pattern = pattern, PatternOffset = props.Stroke.PatternOffset, TippedCornerLimit = props.Stroke.TippedCornerLimit }; } return new PathProperties() { Stroke = stroke, Head = props.Head, Tail = props.Tail, Corners = props.Corners }; } #endregion #region Utilities private GradientFill CloneGradientFill(GradientFill other) { if (other == null) return null; // This is a very fragile gradient fill cloning used since Illustrator // will sometimes refer to another fill using a "xlink:href" attribute. return new GradientFill() { Type = other.Type, Stops = other.Stops, Mode = other.Mode, Opacity = other.Opacity, Addressing = other.Addressing, RadialFocus = other.RadialFocus }; } #endregion #region Simple Attribute Handling int AttribIntVal(string attribName) { return AttribIntVal(attribName, 0); } int AttribIntVal(string attribName, int defaultVal) { string val = styles.Evaluate(attribName); return (val != null) ? int.Parse(val) : defaultVal; } float AttribFloatVal(string attribName) { return AttribFloatVal(attribName, 0.0f); } float AttribFloatVal(string attribName, float defaultVal) { string val = styles.Evaluate(attribName); return (val != null) ? SVGAttribParser.ParseFloat(val) : defaultVal; } float AttribLengthVal(XmlReaderIterator.Node node, string attribName, DimType dimType) { return AttribLengthVal(node, attribName, 0.0f, dimType); } float AttribLengthVal(XmlReaderIterator.Node node, string attribName, float defaultUnitVal, DimType dimType) { var val = styles.Evaluate(attribName); return AttribLengthVal(val, node, attribName, defaultUnitVal, dimType); } float AttribLengthVal(string val, XmlReaderIterator.Node node, string attribName, float defaultUnitVal, DimType dimType) { // For reference: http://www.w3.org/TR/SVG/coords.html#Units if (val == null) return defaultUnitVal; val = val.Trim(); string unitType = "px"; char lastChar = val[val.Length - 1]; if (lastChar == '%') { float number = SVGAttribParser.ParseFloat(val.Substring(0, val.Length - 1)); if (number < 0) throw node.GetException("Number in " + attribName + " cannot be negative"); number /= 100.0f; // If there's an active viewbox, this should be used as the reference size for relative coordinates. // See https://www.w3.org/TR/SVG/coords.html#Units Vector2 vpSize = currentViewBoxSize.Count > 0 ? currentViewBoxSize.Peek() : currentContainerSize.Peek(); switch (dimType) { case DimType.Width: return number * vpSize.x; case DimType.Height: return number * vpSize.y; case DimType.Length: return (number * vpSize.magnitude / SVGLengthFactor); // See http://www.w3.org/TR/SVG/coords.html#Units } } else if (val.Length >= 2) { unitType = val.Substring(val.Length - 2); } if (char.IsDigit(lastChar) || (lastChar == '.')) return SVGAttribParser.ParseFloat(val); // No unit specified.. assume pixels (one px unit is defined to be equal to one user unit) float length = SVGAttribParser.ParseFloat(val.Substring(0, val.Length - 2)); switch (unitType) { case "em": throw new NotImplementedException(); case "ex": throw new NotImplementedException(); case "px": return length; case "in": return 90.0f * length * dpiScale; // "1in" equals "90px" (and therefore 90 user units) case "cm": return 35.43307f * length * dpiScale; // "1cm" equals "35.43307px" (and therefore 35.43307 user units) case "mm": return 3.543307f * length * dpiScale; // "1mm" would be "3.543307px" (3.543307 user units) case "pt": return 1.25f * length * dpiScale; // "1pt" equals "1.25px" (and therefore 1.25 user units) case "pc": return 15.0f * length * dpiScale; // "1pc" equals "15px" (and therefore 15 user units) default: throw new FormatException("Unknown length unit type (" + unitType + ")"); } } #endregion #region Attribute Set Handling void AddToSVGDictionaryIfPossible(XmlReaderIterator.Node node, object vectorElement) { string id = node["id"]; if (!string.IsNullOrEmpty(id)) svgObjects[id] = vectorElement; } Rect ParseViewport(XmlReaderIterator.Node node, SceneNode sceneNode, Vector2 defaultViewportSize) { scenePos.x = AttribLengthVal(node, "x", DimType.Width); scenePos.y = AttribLengthVal(node, "y", DimType.Height); sceneSize.x = AttribLengthVal(node, "width", defaultViewportSize.x, DimType.Width); sceneSize.y = AttribLengthVal(node, "height", defaultViewportSize.y, DimType.Height); // The size could be all 0, in which case we should ignore the viewport sizing logic altogether return new Rect(scenePos, sceneSize); } enum ViewBoxAlign { Min, Mid, Max } enum ViewBoxAspectRatio { DontPreserve, FitLargestDim, FitSmallestDim } struct ViewBoxInfo { public Rect ViewBox; public ViewBoxAspectRatio AspectRatio; public ViewBoxAlign AlignX, AlignY; public bool IsEmpty; } ViewBoxInfo ParseViewBox(XmlReaderIterator.Node node, SceneNode sceneNode, Rect sceneViewport) { var viewBoxInfo = new ViewBoxInfo() { IsEmpty = true }; string viewBoxString = node["viewBox"]; viewBoxString = viewBoxString != null ? viewBoxString.Trim() : null; if (string.IsNullOrEmpty(viewBoxString)) return viewBoxInfo; var viewBoxValues = viewBoxString.Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries); if (viewBoxValues.Length != 4) throw node.GetException("Invalid viewBox specification"); Vector2 viewBoxMin = new Vector2( AttribLengthVal(viewBoxValues[0], node, "viewBox", 0.0f, DimType.Width), AttribLengthVal(viewBoxValues[1], node, "viewBox", 0.0f, DimType.Height)); Vector2 viewBoxSize = new Vector2( AttribLengthVal(viewBoxValues[2], node, "viewBox", sceneViewport.width, DimType.Width), AttribLengthVal(viewBoxValues[3], node, "viewBox", sceneViewport.height, DimType.Height)); viewBoxInfo.ViewBox = new Rect(viewBoxMin, viewBoxSize); ParseViewBoxAspectRatio(node, ref viewBoxInfo); viewBoxInfo.IsEmpty = false; return viewBoxInfo; } void ParseViewBoxAspectRatio(XmlReaderIterator.Node node, ref ViewBoxInfo viewBoxInfo) { viewBoxInfo.AspectRatio = ViewBoxAspectRatio.FitLargestDim; viewBoxInfo.AlignX = ViewBoxAlign.Mid; viewBoxInfo.AlignY = ViewBoxAlign.Mid; string preserveAspectRatioString = node["preserveAspectRatio"]; preserveAspectRatioString = preserveAspectRatioString != null ? preserveAspectRatioString.Trim() : null; bool wantNone = false; if (!string.IsNullOrEmpty(preserveAspectRatioString)) { var preserveAspectRatioValues = preserveAspectRatioString.Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (var value in preserveAspectRatioValues) { switch (value) { case "defer": break; // This is only meaningful on that references another SVG, we don't support that case "none": wantNone = true; break; case "xMinYMin": viewBoxInfo.AlignX = ViewBoxAlign.Min; viewBoxInfo.AlignY = ViewBoxAlign.Min; break; case "xMidYMin": viewBoxInfo.AlignX = ViewBoxAlign.Mid; viewBoxInfo.AlignY = ViewBoxAlign.Min; break; case "xMaxYMin": viewBoxInfo.AlignX = ViewBoxAlign.Max; viewBoxInfo.AlignY = ViewBoxAlign.Min; break; case "xMinYMid": viewBoxInfo.AlignX = ViewBoxAlign.Min; viewBoxInfo.AlignY = ViewBoxAlign.Mid; break; case "xMidYMid": viewBoxInfo.AlignX = ViewBoxAlign.Mid; viewBoxInfo.AlignY = ViewBoxAlign.Mid; break; case "xMaxYMid": viewBoxInfo.AlignX = ViewBoxAlign.Max; viewBoxInfo.AlignY = ViewBoxAlign.Mid; break; case "xMinYMax": viewBoxInfo.AlignX = ViewBoxAlign.Min; viewBoxInfo.AlignY = ViewBoxAlign.Max; break; case "xMidYMax": viewBoxInfo.AlignX = ViewBoxAlign.Mid; viewBoxInfo.AlignY = ViewBoxAlign.Max; break; case "xMaxYMax": viewBoxInfo.AlignX = ViewBoxAlign.Max; viewBoxInfo.AlignY = ViewBoxAlign.Max; break; case "meet": viewBoxInfo.AspectRatio = ViewBoxAspectRatio.FitLargestDim; break; case "slice": viewBoxInfo.AspectRatio = ViewBoxAspectRatio.FitSmallestDim; break; } } } if (wantNone) // Override aspect ratio no matter what other modes are chosen (meet/slice) viewBoxInfo.AspectRatio = ViewBoxAspectRatio.DontPreserve; } void ApplyViewBox(SceneNode sceneNode, ViewBoxInfo viewBoxInfo, Rect sceneViewport) { if ((viewBoxInfo.ViewBox.size == Vector2.zero) || (sceneViewport.size == Vector2.zero)) return; Vector2 scale = Vector2.one, offset = -viewBoxInfo.ViewBox.position; if (viewBoxInfo.AspectRatio == ViewBoxAspectRatio.DontPreserve) { scale = sceneViewport.size / viewBoxInfo.ViewBox.size; } else { scale.x = scale.y = sceneViewport.width / viewBoxInfo.ViewBox.width; bool fitsOnWidth; if (viewBoxInfo.AspectRatio == ViewBoxAspectRatio.FitLargestDim) fitsOnWidth = viewBoxInfo.ViewBox.height * scale.y <= sceneViewport.height; else fitsOnWidth = viewBoxInfo.ViewBox.height * scale.y > sceneViewport.height; Vector2 alignOffset = Vector2.zero; if (fitsOnWidth) { // We fit on the width, so apply the vertical alignment rules if (viewBoxInfo.AlignY == ViewBoxAlign.Mid) alignOffset.y = (sceneViewport.height - viewBoxInfo.ViewBox.height * scale.y) * 0.5f; else if (viewBoxInfo.AlignY == ViewBoxAlign.Max) alignOffset.y = sceneViewport.height - viewBoxInfo.ViewBox.height * scale.y; } else { // We didn't fit on width, meaning we should fit on height and use the wiggle room on width scale.x = scale.y = sceneViewport.height / viewBoxInfo.ViewBox.height; // Apply the horizontal alignment rules if (viewBoxInfo.AlignX == ViewBoxAlign.Mid) alignOffset.x = (sceneViewport.width - viewBoxInfo.ViewBox.width * scale.x) * 0.5f; else if (viewBoxInfo.AlignX == ViewBoxAlign.Max) alignOffset.x = sceneViewport.width - viewBoxInfo.ViewBox.width * scale.x; } offset += alignOffset / scale; } // Aaaaand finally, the transform sceneNode.Transform = sceneNode.Transform * Matrix2D.Scale(scale) * Matrix2D.Translate(offset); } Stroke ParseStrokeAttributeSet(XmlReaderIterator.Node node, out PathCorner strokeCorner, out PathEnding strokeEnding, Inheritance inheritance = Inheritance.Inherited) { var stroke = SVGAttribParser.ParseStrokeAndOpacity(node, svgObjects, styles, inheritance); strokeCorner = PathCorner.Tipped; strokeEnding = PathEnding.Chop; if (stroke != null) { string strokeWidth = styles.Evaluate("stroke-width", inheritance); stroke.HalfThickness = AttribLengthVal(strokeWidth, node, "stroke-width", 1.0f, DimType.Length) * 0.5f; switch (styles.Evaluate("stroke-linecap", inheritance)) { case "butt": strokeEnding = PathEnding.Chop; break; case "square": strokeEnding = PathEnding.Square; break; case "round": strokeEnding = PathEnding.Round; break; } switch (styles.Evaluate("stroke-linejoin", inheritance)) { case "miter": strokeCorner = PathCorner.Tipped; break; case "round": strokeCorner = PathCorner.Round; break; case "bevel": strokeCorner = PathCorner.Beveled; break; } string pattern = styles.Evaluate("stroke-dasharray", inheritance); if (pattern != null && pattern != "none") { string[] entries = pattern.Split(whiteSpaceNumberChars, StringSplitOptions.RemoveEmptyEntries); // If the pattern is odd, then we duplicate it to make it even as per the spec int totalCount = (entries.Length & 1) == 1 ? entries.Length * 2 : entries.Length; stroke.Pattern = new float[totalCount]; for (int i = 0; i < entries.Length; i++) stroke.Pattern[i] = AttribLengthVal(entries[i], node, "stroke-dasharray", 0.0f, DimType.Length); // Duplicate the pattern if (totalCount > entries.Length) { for (int i = 0; i < entries.Length; i++) stroke.Pattern[i + entries.Length] = stroke.Pattern[i]; } var dashOffset = styles.Evaluate("stroke-dashoffset", inheritance); stroke.PatternOffset = AttribLengthVal(dashOffset, node, "stroke-dashoffset", 0.0f, DimType.Length); } var strokeMiterLimit = styles.Evaluate("stroke-miterlimit", inheritance); stroke.TippedCornerLimit = AttribLengthVal(strokeMiterLimit, node, "stroke-miterlimit", 4.0f, DimType.Length); if (stroke.TippedCornerLimit < 1.0f) throw node.GetException("'stroke-miterlimit' should be greater or equal to 1"); } // If stroke is specified return stroke; } void ParseID(XmlReaderIterator.Node node, SceneNode sceneNode) { string id = node["id"]; if (!string.IsNullOrEmpty(id)) { nodeIDs[id] = sceneNode; // Store the style layer of this node since it can be referenced later by a tag nodeStyleLayers[sceneNode] = styles.PeekLayer(); } } float ParseOpacity(SceneNode sceneNode) { float opacity = AttribFloatVal("opacity", 1.0f); if (opacity != 1.0f && sceneNode != null) nodeOpacity[sceneNode] = opacity; return opacity; } void ParseClipAndMask(XmlReaderIterator.Node node, SceneNode sceneNode) { ParseClip(node, sceneNode); ParseMask(node, sceneNode); } void ParseClip(XmlReaderIterator.Node node, SceneNode sceneNode) { string reference = null; string clipPath = styles.Evaluate("clip-path"); if (clipPath != null) reference = SVGAttribParser.ParseURLRef(clipPath); if (reference == null) return; var clipper = SVGAttribParser.ParseRelativeRef(reference, svgObjects) as SceneNode; if (clipper == null && reference.Length > 1 && reference.StartsWith("#")) { // Clipper may be defined later in the file List clips; if (!postponedClip.TryGetValue(reference, out clips)) clips = new List(1); clips.Add(new PostponedClip() { node = sceneNode }); postponedClip[reference.Substring(1)] = clips; return; } var clipperRoot = clipper; bool worldRelative = true; ClipData data; if (clipData.TryGetValue(clipper, out data)) worldRelative = data.WorldRelative; ApplyClipper(clipper, sceneNode, worldRelative); } void ApplyClipper(SceneNode clipper, SceneNode target, bool worldRelative) { SceneNode clipperRoot = clipper; if (!worldRelative) { // If the referenced clip path units is in bounding-box space, we add an intermediate // node to scale the content to the correct size. var rect = VectorUtils.SceneNodeBounds(target); var transform = Matrix2D.Translate(rect.position) * Matrix2D.Scale(rect.size); clipperRoot = new SceneNode() { Children = new List { clipper }, Transform = transform }; } target.Clipper = clipperRoot; } void ParseMask(XmlReaderIterator.Node node, SceneNode sceneNode) { string reference = null; string maskRef = node["mask"]; if (maskRef != null) reference = SVGAttribParser.ParseURLRef(maskRef); if (reference == null) return; var maskPath = SVGAttribParser.ParseRelativeRef(reference, svgObjects) as SceneNode; var maskRoot = maskPath; MaskData data; if (maskData.TryGetValue(maskPath, out data) && !data.ContentWorldRelative) { // If the referenced mask units is in bounding-box space, we add an intermediate // node to scale the content to the correct size. var rect = VectorUtils.SceneNodeBounds(sceneNode); var transform = Matrix2D.Translate(rect.position) * Matrix2D.Scale(rect.size); maskRoot = new SceneNode() { Children = new List { maskPath }, Transform = transform }; } sceneNode.Clipper = maskRoot; } #endregion #region Textures Texture2D DecodeTextureData(string dataURI) { int pos = 5; // Skip "data:" int length = dataURI.Length; int startPos = pos; while (pos < length && dataURI[pos] != ';' && dataURI[pos] != ',') ++pos; var mediaType = dataURI.Substring(startPos, pos-startPos); if (mediaType != "image/png" && mediaType != "image/jpeg") return null; while (pos < length && dataURI[pos] != ',') ++pos; ++pos; // Skip ',' if (pos >= length) return null; var data = Convert.FromBase64String(dataURI.Substring(pos)); var tex = new Texture2D(1, 1); if (tex.LoadImage(data)) return tex; return null; } #endregion #region Post-processing void PostProcess(SceneNode root) { AdjustFills(root); } struct HierarchyUpdate { public SceneNode Parent; public SceneNode NewNode; public SceneNode ReplaceNode; } void AdjustFills(SceneNode root) { var hierarchyUpdates = new List(); // Adjust fills on all objects foreach (var nodeInfo in VectorUtils.WorldTransformedSceneNodes(root, nodeOpacity)) { if (nodeInfo.Node.Shapes == null) continue; foreach (var shape in nodeInfo.Node.Shapes) { if (shape.Fill != null) { // This fill may be a placeholder for postponed reference, try to resolve it here. string reference; if (postponedFills.TryGetValue(shape.Fill, out reference)) { var fill = SVGAttribParser.ParseRelativeRef(reference, svgObjects) as IFill; if (fill != null) shape.Fill = fill; } } var stroke = shape.PathProps.Stroke; if (stroke != null && stroke.Fill is GradientFill) { var fillTransform = Matrix2D.identity; AdjustGradientFill(nodeInfo.Node, nodeInfo.WorldTransform, stroke.Fill, shape.Contours, ref fillTransform); stroke.FillTransform = fillTransform; } if (shape.Fill is GradientFill) { var fillTransform = Matrix2D.identity; AdjustGradientFill(nodeInfo.Node, nodeInfo.WorldTransform, shape.Fill, shape.Contours, ref fillTransform); shape.FillTransform = fillTransform; } else if (shape.Fill is PatternFill) { var fillNode = AdjustPatternFill(nodeInfo.Node, nodeInfo.WorldTransform, shape); if (fillNode != null) { hierarchyUpdates.Add(new HierarchyUpdate() { Parent = nodeInfo.Parent, NewNode = fillNode, ReplaceNode = nodeInfo.Node }); } } } } foreach (var update in hierarchyUpdates) { var index = update.Parent.Children.IndexOf(update.ReplaceNode); update.Parent.Children.RemoveAt(index); update.Parent.Children.Insert(index, update.NewNode); } } void AdjustGradientFill(SceneNode node, Matrix2D worldTransform, IFill fill, BezierContour[] contours, ref Matrix2D computedTransform) { var gradientFill = fill as GradientFill; if (fill == null || contours == null || contours.Length == 0) return; var min = new Vector2(float.MaxValue, float.MaxValue); var max = new Vector2(-float.MaxValue, -float.MaxValue); foreach (var contour in contours) { var bbox = VectorUtils.Bounds(contour.Segments); min = Vector2.Min(min, bbox.min); max = Vector2.Max(max, bbox.max); } Rect bounds = new Rect(min, max - min); GradientExData extInfo = (GradientExData)gradientExInfo[gradientFill]; var containerSize = nodeGlobalSceneState[node].ContainerSize; Matrix2D gradTransform = Matrix2D.identity; currentContainerSize.Push(extInfo.WorldRelative ? containerSize : Vector2.one); // If the fill is object relative, then the dimensions will come to us in // a normalized space, we must adjust those to the object's dimensions if (extInfo is LinearGradientExData) { // In SVG, linear gradients are expressed using two vectors. A vector and normal. The vector determines // the direction where the gradient increases. The normal determines the slant of the gradient along the vector. // Due to transformations, it is possible that those two vectors (the gradient vector and its normal) are not // actually perpendicular. That's why a skew transformation is involved here. // VectorScene just maps linear gradients from 0 to 1 across the entire bounding box width, so we // need to figure out a super transformation that takes those simply-mapped UVs and have them express // the linear gradient with its slant and all the fun involved. var linGradEx = (LinearGradientExData)extInfo; Vector2 lineStart = new Vector2( AttribLengthVal(linGradEx.X1, null, null, 0.0f, DimType.Width), AttribLengthVal(linGradEx.Y1, null, null, 0.0f, DimType.Height)); Vector2 lineEnd = new Vector2( AttribLengthVal(linGradEx.X2, null, null, currentContainerSize.Peek().x, DimType.Width), AttribLengthVal(linGradEx.Y2, null, null, 0.0f, DimType.Height)); var gradientVector = lineEnd - lineStart; float gradientVectorInvLength = 1.0f / gradientVector.magnitude; var scale = Matrix2D.Scale(new Vector2(bounds.width * gradientVectorInvLength, bounds.height * gradientVectorInvLength)); var rotation = Matrix2D.RotateLH(Mathf.Atan2(gradientVector.y, gradientVector.x)); var offset = Matrix2D.Translate(-lineStart); gradTransform = scale * rotation * offset; } else if (extInfo is RadialGradientExData) { // VectorScene positions radial gradiants at the center of the bbox, and picks the radii (not one radius, but two) // to fill the space between the center and the two edges (horizontal and vertical). So in the general case // the radial is actually an ellipsoid. So we need to do an SRT transformation to position the radial gradient according // to the SVG center point and radius var radGradEx = (RadialGradientExData)extInfo; Vector2 halfCurrentContainerSize = currentContainerSize.Peek() * 0.5f; Vector2 center = new Vector2( AttribLengthVal(radGradEx.Cx, null, null, halfCurrentContainerSize.x, DimType.Width), AttribLengthVal(radGradEx.Cy, null, null, halfCurrentContainerSize.y, DimType.Height)); Vector2 focus = new Vector2( AttribLengthVal(radGradEx.Fx, null, null, center.x, DimType.Width), AttribLengthVal(radGradEx.Fy, null, null, center.y, DimType.Height)); float radius = AttribLengthVal(radGradEx.R, null, null, halfCurrentContainerSize.magnitude / SVGLengthFactor, DimType.Length); // This block below tells that radial focus cannot change per object, but is realized correctly for the first object // that requests this gradient. If the gradient is using object-relative coordinates to specify the focus location, // then only the first object will look correct, and the rest will potentially not look right. The alternative is // to detect if it is necessary and generate a new atlas entry for it if (!radGradEx.Parsed) { // VectorGradientFill radialFocus is (-1,1) relative to the outer circle gradientFill.RadialFocus = (focus - center) / radius; if (gradientFill.RadialFocus.sqrMagnitude > 1.0f - VectorUtils.Epsilon) gradientFill.RadialFocus = gradientFill.RadialFocus.normalized * (1.0f - VectorUtils.Epsilon); // Stick within the unit circle radGradEx.Parsed = true; } gradTransform = Matrix2D.Scale(bounds.size * 0.5f / radius) * Matrix2D.Translate(new Vector2(radius, radius) - center); } else { Debug.LogError("Unsupported gradient type: " + extInfo); } currentContainerSize.Pop(); var uvToWorld = extInfo.WorldRelative ? Matrix2D.Translate(bounds.min) * Matrix2D.Scale(bounds.size) : Matrix2D.identity; var boundsInv = new Vector2(1.0f / bounds.width, 1.0f / bounds.height); computedTransform = Matrix2D.Scale(boundsInv) * gradTransform * extInfo.FillTransform.Inverse() * uvToWorld; } SceneNode AdjustPatternFill(SceneNode node, Matrix2D worldTransform, Shape shape) { PatternFill patternFill = shape.Fill as PatternFill; if (patternFill == null || Mathf.Abs(patternFill.Rect.width) < VectorUtils.Epsilon || Mathf.Abs(patternFill.Rect.height) < VectorUtils.Epsilon) { return null; } var data = patternData[patternFill.Pattern]; var nodeBounds = VectorUtils.SceneNodeBounds(node); var patternRect = patternFill.Rect; if (!data.WorldRelative) { patternRect.position *= nodeBounds.size; patternRect.size *= nodeBounds.size; } // The pattern fill will create a new clipped node containing the repeating pattern // as well as a sibling containing the original node. This will replace the original node. var replacementNode = new SceneNode() { Transform = node.Transform, Children = new List(2) }; node.Transform = Matrix2D.identity; // The pattern node will be wrapped in a scaling node if content isn't world relative var patternNode = patternFill.Pattern; if (!data.ContentWorldRelative) { patternNode = new SceneNode() { Transform = Matrix2D.Scale(nodeBounds.size), Children = new List { patternFill.Pattern } }; } PostProcess(patternNode); // This will take care of adjusting gradients/inner-patterns // Duplicate the filling pattern var grid = new SceneNode() { Transform = data.PatternTransform, Children = new List(20) }; var fill = new SceneNode() { Transform = Matrix2D.identity, Children = new List { grid }, Clipper = node }; // SVG patterns are clipped in their respective "boxes" var clippingBox = new Shape(); VectorUtils.MakeRectangleShape(clippingBox, new Rect(0,0,patternRect.width, patternRect.height)); var box = new SceneNode() { Transform = Matrix2D.identity, Shapes = new List { clippingBox } }; // Compute the bounds of the shape to be filled, taking into account the pattern transform var bounds = VectorUtils.SceneNodeBounds(node); var invPatternTransform = data.PatternTransform.Inverse(); var boundVerts = new Vector2[] { invPatternTransform * new Vector2(bounds.xMin, bounds.yMin), invPatternTransform * new Vector2(bounds.xMax, bounds.yMin), invPatternTransform * new Vector2(bounds.xMax, bounds.yMax), invPatternTransform * new Vector2(bounds.xMin, bounds.yMax) }; bounds = VectorUtils.Bounds(boundVerts); const int kMaxReps = 5000; float xCount = bounds.xMax / patternRect.width; float yCount = bounds.yMax / patternRect.height; if (Mathf.Abs(patternRect.width) < VectorUtils.Epsilon || Mathf.Abs(patternRect.height) < VectorUtils.Epsilon || (xCount*yCount) > kMaxReps) { Debug.LogWarning("Ignoring pattern which would result in too many repetitions"); return null; } // Start the pattern filling process var offset = patternRect.position; float xStart = (int)(bounds.x / patternRect.width) * patternRect.width - patternRect.width; float yStart = (int)(bounds.y / patternRect.height) * patternRect.height - patternRect.height; for (float y = yStart; y < bounds.yMax; y += patternRect.height) { for (float x = xStart; x < bounds.xMax; x += patternRect.width) { var pattern = new SceneNode() { Transform = Matrix2D.Translate(new Vector2(x, y) + offset), Children = new List { patternNode }, Clipper = box }; grid.Children.Add(pattern); } } replacementNode.Children.Add(fill); replacementNode.Children.Add(node); return replacementNode; } void RemoveInvisibleNodes() { foreach (var n in invisibleNodes) { if (n.parent.Children != null) n.parent.Children.Remove(n.node); } } #endregion delegate void ElemHandler(); class Handlers : Dictionary { public Handlers(int capacity) : base(capacity) {} } bool ShouldDeclareSupportedChildren(XmlReaderIterator.Node node) { return !subTags.ContainsKey(node.Name); } void SupportElems(XmlReaderIterator.Node node, params ElemHandler[] handlers) { var elems = new Handlers(handlers.Length); foreach (var h in handlers) elems[h.Method.Name] = h; subTags[node.Name] = elems; } static char[] whiteSpaceNumberChars = " \r\n\t,".ToCharArray(); enum DimType { Width, Height, Length }; XmlReaderIterator docReader; Scene scene; float dpiScale; int windowWidth, windowHeight; Vector2 scenePos, sceneSize; SVGDictionary svgObjects = new SVGDictionary(); // Named elements are looked up in this Dictionary subTags = new Dictionary(); // For each element, the set of elements supported as its children Dictionary gradientExInfo = new Dictionary(); Dictionary symbolViewBoxes = new Dictionary(); Dictionary nodeGlobalSceneState = new Dictionary(); Dictionary nodeOpacity = new Dictionary(); Dictionary nodeIDs = new Dictionary(); Dictionary nodeStyleLayers = new Dictionary(); Dictionary clipData = new Dictionary(); Dictionary patternData = new Dictionary(); Dictionary maskData = new Dictionary(); Dictionary> postponedSymbolData = new Dictionary>(); Dictionary> postponedStopData = new Dictionary>(); Dictionary> postponedClip = new Dictionary>(); SVGPostponedFills postponedFills = new SVGPostponedFills(); List invisibleNodes = new List(); Stack currentContainerSize = new Stack(); Stack currentViewBoxSize = new Stack(); Stack currentSceneNode = new Stack(); GradientFill currentGradientFill; string currentGradientId; string currentGradientLink; ElemHandler[] allElems; HashSet elemsToAddToHierarchy; SVGStyleResolver styles = new SVGStyleResolver(); bool applyRootViewBox; internal Rect sceneViewport; struct NodeGlobalSceneState { public Vector2 ContainerSize; } class GradientExData { public bool WorldRelative; public Matrix2D FillTransform; } class LinearGradientExData : GradientExData { public string X1, Y1, X2, Y2; } class RadialGradientExData : GradientExData { public bool Parsed; public string Cx, Cy, Fx, Fy, R; } struct ClipData { public bool WorldRelative; } struct PatternData { public bool WorldRelative; public bool ContentWorldRelative; public Matrix2D PatternTransform; } struct MaskData { public bool WorldRelative; public bool ContentWorldRelative; } struct NodeWithParent { public SceneNode node; public SceneNode parent; } struct NodeReferenceData { public SceneNode node; public Rect viewport; public string id; } struct PostponedStopData { public GradientFill fill; } struct PostponedClip { public SceneNode node; } } internal enum Inheritance { None, Inherited } internal class SVGStyleResolver { public void PushNode(XmlReaderIterator.Node node) { var nodeData = new NodeData(); nodeData.node = node; nodeData.name = node.Name; var klass = node["class"]; if (klass != null) nodeData.classes = node["class"].Split(' ').Select(x => x.Trim()).ToList(); else nodeData.classes = new List(); nodeData.classes = SortedClasses(nodeData.classes).ToList(); nodeData.id = node["id"]; var layer = new StyleLayer(); layer.nodeData = nodeData; layer.attributeSheet = node.GetAttributes(); layer.styleSheet = new SVGStyleSheet(); var cssText = node["style"]; if (cssText != null) { var props = SVGStyleSheetUtils.ParseInline(cssText); layer.styleSheet[node.Name] = props; } PushLayer(layer); } public void PopNode() { PopLayer(); } public void PushLayer(StyleLayer layer) { layers.Add(layer); } public void PopLayer() { if (layers.Count == 0) throw SVGFormatException.StackError; layers.RemoveAt(layers.Count - 1); } public StyleLayer PeekLayer() { if (layers.Count == 0) return null; return layers[layers.Count-1]; } public void SaveLayerForSceneNode(SceneNode node) { nodeLayers[node] = PeekLayer(); } public StyleLayer GetLayerForScenNode(SceneNode node) { if (!nodeLayers.ContainsKey(node)) return null; return nodeLayers[node]; } public void SetGlobalStyleSheet(SVGStyleSheet sheet) { foreach (var sel in sheet.selectors) globalStyleSheet[sel] = sheet[sel]; } public string Evaluate(string attribName, Inheritance inheritance = Inheritance.None) { for (int i = layers.Count-1; i >= 0; --i) { string attrib = null; if (LookupStyleOrAttribute(layers[i], attribName, inheritance, out attrib)) return attrib; if (inheritance == Inheritance.None) break; } return null; } private bool LookupStyleOrAttribute(StyleLayer layer, string attribName, Inheritance inheritance, out string attrib) { // Try to match a CSS style first if (LookupProperty(layer.nodeData, attribName, layer.styleSheet, out attrib)) return true; // Try to match a global CSS style if (LookupProperty(layer.nodeData, attribName, globalStyleSheet, out attrib)) return true; // Else, fallback on attribute if (layer.attributeSheet.ContainsKey(attribName)) { attrib = layer.attributeSheet[attribName]; return true; } return false; } private bool LookupProperty(NodeData nodeData, string attribName, SVGStyleSheet sheet, out string val) { var id = string.IsNullOrEmpty(nodeData.id) ? null : "#" + nodeData.id; var name = string.IsNullOrEmpty(nodeData.name) ? null : nodeData.name; if (LookupPropertyInSheet(sheet, attribName, id, out val)) return true; foreach (var c in nodeData.classes) { var klass = "." + c; if (LookupPropertyInSheet(sheet, attribName, klass, out val)) return true; } if (LookupPropertyInSheet(sheet, attribName, name, out val)) return true; if (LookupPropertyInSheet(sheet, attribName, "*", out val)) return true; val = null; return false; } private bool LookupPropertyInSheet(SVGStyleSheet sheet, string attribName, string selector, out string val) { if (selector == null) { val = null; return false; } if (sheet.selectors.Contains(selector)) { var props = sheet[selector]; if (props.ContainsKey(attribName)) { val = props[attribName]; return true; } } val = null; return false; } private IEnumerable SortedClasses(List classes) { // We may not have parsed the global sheets yet (happens when setting a class on the root element). if (globalStyleSheet.selectors.Count() == 0) { foreach (var klass in classes) yield return klass; } // We match classes in reverse order of their appearance. This isn't conformant to CSS selectors priority, // but this works well enough for auto-generated CSS styles. foreach (var sel in globalStyleSheet.selectors.Reverse()) { if (sel[0] != '.') continue; var klass = sel.Substring(1); if (classes.Contains(klass)) yield return klass; } } public struct NodeData { public XmlReaderIterator.Node node; public string name; public List classes; public string id; } public class StyleLayer { public SVGStyleSheet styleSheet; public SVGPropertySheet attributeSheet; public NodeData nodeData; } private List layers = new List(); private SVGStyleSheet globalStyleSheet = new SVGStyleSheet(); private Dictionary nodeLayers = new Dictionary(); } internal class SVGAttribParser { public static List ParsePath(XmlReaderIterator.Node node) { string path = node["d"]; if (string.IsNullOrEmpty(path)) return null; try { return (new SVGAttribParser(path, AttribPath.Path)).contours; } catch (Exception e) { throw node.GetException(e.Message); } } public static Matrix2D ParseTransform(XmlReaderIterator.Node node) { return ParseTransform(node, "transform"); } public static Matrix2D ParseTransform(XmlReaderIterator.Node node, string attribName) { // Transforms aren't part of styling and shouldn't be evaluated, // they have to be specified as node attributes string transform = node[attribName]; if (string.IsNullOrEmpty(transform)) return Matrix2D.identity; try { return (new SVGAttribParser(transform, attribName, AttribTransform.Transform)).transform; } catch (Exception e) { throw node.GetException(e.Message); } } public static IFill ParseFill(XmlReaderIterator.Node node, SVGDictionary dict, SVGPostponedFills postponedFills, SVGStyleResolver styles, Inheritance inheritance = Inheritance.Inherited) { bool isDefaultFill; return ParseFill(node, dict, postponedFills, styles, inheritance, out isDefaultFill); } public static IFill ParseFill(XmlReaderIterator.Node node, SVGDictionary dict, SVGPostponedFills postponedFills, SVGStyleResolver styles, Inheritance inheritance, out bool isDefaultFill) { string opacityAttrib = styles.Evaluate("fill-opacity", inheritance); float opacity = (opacityAttrib != null) ? ParseFloat(opacityAttrib) : 1.0f; string fillMode = styles.Evaluate("fill-rule", inheritance); FillMode mode = FillMode.NonZero; if (fillMode != null) { if (fillMode == "nonzero") mode = FillMode.NonZero; else if (fillMode == "evenodd") mode = FillMode.OddEven; else throw new Exception("Unknown fill-rule: " + fillMode); } try { var fill = styles.Evaluate("fill", inheritance); isDefaultFill = (fill == null && opacityAttrib == null); return (new SVGAttribParser(fill, "fill", opacity, mode, dict, postponedFills)).fill; } catch (Exception e) { throw node.GetException(e.Message); } } public static Stroke ParseStrokeAndOpacity(XmlReaderIterator.Node node, SVGDictionary dict, SVGStyleResolver styles, Inheritance inheritance = Inheritance.Inherited) { string strokeAttrib = styles.Evaluate("stroke", inheritance); if (string.IsNullOrEmpty(strokeAttrib)) return null; // If stroke is not specified, no other stroke properties matter string opacityAttrib = styles.Evaluate("stroke-opacity", inheritance); float opacity = (opacityAttrib != null) ? ParseFloat(opacityAttrib) : 1.0f; IFill strokeFill = null; try { strokeFill = (new SVGAttribParser(strokeAttrib, "stroke", opacity, FillMode.NonZero, dict, null)).fill; } catch (Exception e) { throw node.GetException(e.Message); } if (strokeFill == null) return null; return new Stroke() { Fill = strokeFill }; } public static Color ParseColor(string colorString) { if (colorString[0] == '#') { // Hex format var hexVal = UInt32.Parse(colorString.Substring(1), NumberStyles.HexNumber); if (colorString.Length == 4) { // #ABC >> #AABBCC return new Color( ((((hexVal >> 8) & 0xF) << 0) | (((hexVal >> 8) & 0xF) << 4)) / 255.0f, ((((hexVal >> 4) & 0xF) << 0) | (((hexVal >> 4) & 0xF) << 4)) / 255.0f, ((((hexVal >> 0) & 0xF) << 0) | (((hexVal >> 0) & 0xF) << 4)) / 255.0f); } else { // #ABCDEF return new Color( ((hexVal >> 16) & 0xFF) / 255.0f, ((hexVal >> 8) & 0xFF) / 255.0f, ((hexVal >> 0) & 0xFF) / 255.0f); } } if (colorString.StartsWith("rgb(") && colorString.EndsWith(")")) { string numbersString = colorString.Substring(4, colorString.Length-5); string[] numbers = numbersString.Split(new char[] { ',', '%' }, StringSplitOptions.RemoveEmptyEntries); if (numbers.Length != 3) throw new Exception("Invalid rgb() color specification"); float divisor = colorString.Contains("%") ? 100.0f : 255.0f; return new Color(Byte.Parse(numbers[0]) / divisor, Byte.Parse(numbers[1]) / divisor, Byte.Parse(numbers[2]) / divisor); } else if (colorString.StartsWith("rgba(") && colorString.EndsWith(")")) { string numbersString = colorString.Substring(5, colorString.Length-6); string[] numbers = numbersString.Split(new char[] { ',', '%' }, StringSplitOptions.RemoveEmptyEntries); if (numbers.Length != 4) throw new Exception("Invalid rgba() color specification"); float divisor = colorString.Contains("%") ? 100.0f : 255.0f; return new Color( Byte.Parse(numbers[0]) / divisor, Byte.Parse(numbers[1]) / divisor, Byte.Parse(numbers[2]) / divisor, divisor == 100.0f ? Byte.Parse(numbers[3]) / divisor : ParseFloat(numbers[3])); } // Named color if (namedColors == null) namedColors = new NamedWebColorDictionary(); return namedColors[colorString.ToLower()]; } public static string ParseURLRef(string url) { if (url.StartsWith("url(") && url.EndsWith(")")) return url.Substring(4, url.Length - 5); return null; } public static object ParseRelativeRef(string iri, SVGDictionary dict) { if (iri == null) return null; if (!iri.StartsWith("#")) throw new Exception("Unsupported reference type (" + iri + ")"); iri = iri.Substring(1); object obj; dict.TryGetValue(iri, out obj); return obj; } public static string CleanIri(string iri) { if (iri == null) return null; if (!iri.StartsWith("#")) throw new Exception("Unsupported reference type (" + iri + ")"); iri = iri.Substring(1); return iri; } SVGAttribParser(string attrib, AttribPath attribPath) { attribName = "path"; attribString = attrib; NextPathCommand(true); if (pathCommand != 'm' && pathCommand != 'M') throw new Exception("Path must start with a MoveTo pathCommand"); char lastCmdNoCase = '\0'; Vector2 lastQCtrlPoint = Vector2.zero; while (NextPathCommand() != (char)0) { bool relative = (pathCommand >= 'a') && (pathCommand <= 'z'); char cmdNoCase = char.ToLower(pathCommand); if (cmdNoCase == 'm') // Move-to { penPos = NextVector2(relative); pathCommand = relative ? 'l' : 'L'; // After a move-to, we automatically switch to a line-to of the same relativity ConcludePath(false); } else if (cmdNoCase == 'z') // ClosePath { if (currentContour.First != null) penPos = currentContour.First.Value.P0; ConcludePath(true); } else if (cmdNoCase == 'l') // Line-to { var to = NextVector2(relative); if ((to - penPos).magnitude > VectorUtils.Epsilon) currentContour.AddLast(VectorUtils.MakeLine(penPos, to)); penPos = to; } else if (cmdNoCase == 'h') // Horizontal-line-to { float x = relative ? penPos.x + NextFloat() : NextFloat(); var to = new Vector2(x, penPos.y); if ((to - penPos).magnitude > VectorUtils.Epsilon) currentContour.AddLast(VectorUtils.MakeLine(penPos, to)); penPos = to; } else if (cmdNoCase == 'v') // Vertical-line-to { float y = relative ? penPos.y + NextFloat() : NextFloat(); var to = new Vector2(penPos.x, y); if ((to - penPos).magnitude > VectorUtils.Epsilon) currentContour.AddLast(VectorUtils.MakeLine(penPos, to)); penPos = to; } else if (cmdNoCase == 'c' || cmdNoCase == 'q') // Cubic-bezier-curve or quadratic-bezier-curve { // If relative, the pen position is on P0 and is only moved to P3 BezierSegment bs = new BezierSegment(); bs.P0 = penPos; bs.P1 = NextVector2(relative); if (cmdNoCase == 'c') bs.P2 = NextVector2(relative); bs.P3 = NextVector2(relative); if (cmdNoCase == 'q') { lastQCtrlPoint = bs.P1; var t = 2.0f/3.0f; bs.P1 = bs.P0 + t * (lastQCtrlPoint - bs.P0); bs.P2 = bs.P3 + t * (lastQCtrlPoint - bs.P3); } penPos = bs.P3; if (!VectorUtils.IsEmptySegment(bs)) currentContour.AddLast(bs); } else if (cmdNoCase == 's' || cmdNoCase == 't') // Smooth cubic-bezier-curve or smooth quadratic-bezier-curve { Vector2 reflectedP1 = penPos; if (currentContour.Count > 0 && (lastCmdNoCase == 'c' || lastCmdNoCase == 'q' || lastCmdNoCase == 's' || lastCmdNoCase == 't')) reflectedP1 += currentContour.Last.Value.P3 - ((lastCmdNoCase == 'q' || lastCmdNoCase == 't') ? lastQCtrlPoint : currentContour.Last.Value.P2); // If relative, the pen position is on P0 and is only moved to P3 BezierSegment bs = new BezierSegment(); bs.P0 = penPos; bs.P1 = reflectedP1; if (cmdNoCase == 's') bs.P2 = NextVector2(relative); bs.P3 = NextVector2(relative); if (cmdNoCase == 't') { lastQCtrlPoint = bs.P1; var t = 2.0f / 3.0f; bs.P1 = bs.P0 + t * (lastQCtrlPoint - bs.P0); bs.P2 = bs.P3 + t * (lastQCtrlPoint - bs.P3); } penPos = bs.P3; if (!VectorUtils.IsEmptySegment(bs)) currentContour.AddLast(bs); } else if (cmdNoCase == 'a') // Elliptical-arc-to { Vector2 radii = NextVector2(); float xAxisRotation = NextFloat(); bool largeArcFlag = NextBool(); bool sweepFlag = NextBool(); Vector2 to = NextVector2(relative); if (radii.magnitude <= VectorUtils.Epsilon) { if ((to - penPos).magnitude > VectorUtils.Epsilon) currentContour.AddLast(VectorUtils.MakeLine(penPos, to)); } else { var ellipsePath = VectorUtils.BuildEllipsePath(penPos, to, -xAxisRotation * Mathf.Deg2Rad, radii.x, radii.y, largeArcFlag, sweepFlag); foreach (var seg in VectorUtils.SegmentsInPath(ellipsePath)) currentContour.AddLast(seg); } penPos = to; } lastCmdNoCase = cmdNoCase; } // While commands exist in the string ConcludePath(false); } SVGAttribParser(string attrib, string attribNameVal, AttribTransform attribTransform) { attribString = attrib; attribName = attribNameVal; transform = Matrix2D.identity; while (stringPos < attribString.Length) { int cmdPos = stringPos; var trasformCommand = NextStringCommand(); if (string.IsNullOrEmpty(trasformCommand)) return; SkipSymbol('('); if (trasformCommand == "matrix") { Matrix2D mat = new Matrix2D(); mat.m00 = NextFloat(); mat.m10 = NextFloat(); mat.m01 = NextFloat(); mat.m11 = NextFloat(); mat.m02 = NextFloat(); mat.m12 = NextFloat(); transform *= mat; } else if (trasformCommand == "translate") { float x = NextFloat(); float y = 0; if (!PeekSymbol(')')) y = NextFloat(); transform *= Matrix2D.Translate(new Vector2(x, y)); } else if (trasformCommand == "scale") { float x = NextFloat(); float y = x; if (!PeekSymbol(')')) y = NextFloat(); transform *= Matrix2D.Scale(new Vector2(x, y)); } else if (trasformCommand == "rotate") { float a = NextFloat() * Mathf.Deg2Rad; float cx = 0, cy = 0; if (!PeekSymbol(')')) { cx = NextFloat(); cy = NextFloat(); } transform *= Matrix2D.Translate(new Vector2(cx, cy)) * Matrix2D.RotateLH(-a) * Matrix2D.Translate(new Vector2(-cx, -cy)); } else if ((trasformCommand == "skewX") || (trasformCommand == "skewY")) { float a = Mathf.Tan(NextFloat() * Mathf.Deg2Rad); Matrix2D mat = Matrix2D.identity; if (trasformCommand == "skewY") mat.m10 = a; else mat.m01 = a; transform *= mat; } else throw new Exception("Unknown transform command at " + cmdPos + " in trasform specification"); SkipSymbol(')'); } } SVGAttribParser(string attrib, string attribName, float opacity, FillMode mode, SVGDictionary dict, SVGPostponedFills postponedFills, bool allowReference = true) { this.attribName = attribName; if (string.IsNullOrEmpty(attrib)) { if (opacity < 1.0f) fill = new SolidFill() { Color = new Color(0, 0, 0, opacity) }; else fill = dict[mode == FillMode.NonZero ? SVGDocument.StockBlackNonZeroFillName : SVGDocument.StockBlackOddEvenFillName] as IFill; return; } if (attrib == "none" || attrib == "transparent") return; if (attrib == "currentColor") { Debug.LogError("currentColor is not supported as a " + attribName + " value"); return; } string[] paintParts = attrib.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (allowReference) { string reference = ParseURLRef(paintParts[0]); if (reference != null) { fill = ParseRelativeRef(reference, dict) as IFill; if (fill == null) { if (paintParts.Length > 1) { fill = (new SVGAttribParser(paintParts[1], attribName, opacity, mode, dict, postponedFills, false)).fill; } else if (postponedFills != null) { // The reference doesn't exist, but may be defined later in the file. // Make a dummy fill to be replaced later. fill = new SolidFill() { Color = Color.clear }; postponedFills[fill] = reference; } } if (fill != null) fill.Opacity = opacity; return; } } var clr = ParseColor(string.Join("", paintParts)); clr.a *= opacity; if (paintParts.Length > 1) { // TODO: Support ICC-Color } fill = new SolidFill() { Color = clr, Mode = mode }; } void ConcludePath(bool joinEnds) { // No need to manually close the path with the last line. It is implied. //if (joinEnds && currentPath.Count >= 2) //{ // BezierSegment bs = new BezierSegment(); // bs.MakeLine(currentPath.Last.Value.P3, currentPath.First.Value.P0); // currentPath.AddLast(bs); //} if (currentContour.Count > 0) { BezierContour contour = new BezierContour(); contour.Closed = joinEnds && (currentContour.Count >= 1); contour.Segments = new BezierPathSegment[currentContour.Count + 1]; int index = 0; foreach (var bs in currentContour) contour.Segments[index++] = new BezierPathSegment() { P0 = bs.P0, P1 = bs.P1, P2 = bs.P2 }; var connect = VectorUtils.MakeLine(currentContour.Last.Value.P3, contour.Segments[0].P0); contour.Segments[index] = new BezierPathSegment() { P0 = connect.P0, P1 = connect.P1, P2 = connect.P2 }; contours.Add(contour); } currentContour.Clear(); // Restart a new path } Vector2 NextVector2(bool relative = false) { var v = new Vector2(NextFloat(), NextFloat()); return relative ? v + penPos : v; } float NextFloat() { SkipWhitespaces(); if (stringPos >= attribString.Length) throw new Exception(attribName + " specification ended before sufficing numbers required by the last pathCommand"); int startPos = stringPos; if (attribString[stringPos] == '-') stringPos++; // Skip over the negative sign if it exists bool gotPeriod = false; bool gotE = false; while (stringPos < attribString.Length) { char c = attribString[stringPos]; if (!gotPeriod && (c == '.')) { gotPeriod = true; stringPos++; continue; } if (!gotE && ((c == 'e') || (c == 'E'))) { gotE = true; stringPos++; if ((stringPos < attribString.Length) && (attribString[stringPos] == '-')) stringPos++; // Skip over the negative sign if it exists for the e continue; } if (!char.IsDigit(c)) break; stringPos++; } if ((stringPos - startPos == 0) || ((stringPos - startPos == 1) && attribString[startPos] == '-')) throw new Exception("Missing number at " + startPos + " in " + attribName + " specification"); return ParseFloat(attribString.Substring(startPos, stringPos - startPos)); } internal static float ParseFloat(string s) { return float.Parse(s, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture); } bool NextBool() { return Mathf.Abs(NextFloat()) > VectorUtils.Epsilon; } char NextPathCommand(bool noCommandInheritance = false) { SkipWhitespaces(); if (stringPos >= attribString.Length) return (char)0; char newCmd = attribString[stringPos]; if ((newCmd >= 'a' && newCmd <= 'z') || (newCmd >= 'A' && newCmd <= 'Z')) { pathCommand = newCmd; stringPos++; return newCmd; } if (!noCommandInheritance && (char.IsDigit(newCmd) || (newCmd == '.') || (newCmd == '-'))) return pathCommand; // Stepped onto a number, which means we keep the last pathCommand throw new Exception("Unexpected character at " + stringPos + " in path specification"); } string NextStringCommand() { SkipWhitespaces(); if (stringPos >= attribString.Length) return null; int startPos = stringPos; while (stringPos < attribString.Length) { char c = attribString[stringPos]; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) stringPos++; else break; } if (stringPos - startPos == 0) throw new Exception("Unexpected character at " + stringPos + " in " + attribName + " specification"); return attribString.Substring(startPos, stringPos - startPos); } void SkipSymbol(char s) { SkipWhitespaces(); if (stringPos >= attribString.Length || (attribString[stringPos] != s)) throw new Exception("Expected " + s + " at " + stringPos + " of " + attribName + " specification"); stringPos++; } bool PeekSymbol(char s) { SkipWhitespaces(); return (stringPos < attribString.Length) && (attribString[stringPos] == s); } void SkipWhitespaces() { while (stringPos < attribString.Length) { switch (attribString[stringPos]) { case ' ': case '\r': case '\n': case '\t': case ',': stringPos++; break; default: return; } } } enum AttribPath { Path }; enum AttribTransform { Transform }; enum AttribStroke { Stroke }; // Path data LinkedList currentContour = new LinkedList(); List contours = new List(); Vector2 penPos; string attribString; char pathCommand; // Transform data Matrix2D transform; // Fill data IFill fill; // Parsing data string attribName; int stringPos; static NamedWebColorDictionary namedColors; } class NamedWebColorDictionary : Dictionary { public NamedWebColorDictionary() { this["aliceblue"] = new Color(240.0f / 255.0f, 248.0f / 255.0f, 255.0f / 255.0f); this["antiquewhite"] = new Color(250.0f / 255.0f, 235.0f / 255.0f, 215.0f / 255.0f); this["aqua"] = new Color(0.0f / 255.0f, 255.0f / 255.0f, 255.0f / 255.0f); this["aquamarine"] = new Color(127.0f / 255.0f, 255.0f / 255.0f, 212.0f / 255.0f); this["azure"] = new Color(240.0f / 255.0f, 255.0f / 255.0f, 255.0f / 255.0f); this["beige"] = new Color(245.0f / 255.0f, 245.0f / 255.0f, 220.0f / 255.0f); this["bisque"] = new Color(255.0f / 255.0f, 228.0f / 255.0f, 196.0f / 255.0f); this["black"] = new Color(0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f); this["blanchedalmond"] = new Color(255.0f / 255.0f, 235.0f / 255.0f, 205.0f / 255.0f); this["blue"] = new Color(0.0f / 255.0f, 0.0f / 255.0f, 255.0f / 255.0f); this["blueviolet"] = new Color(138.0f / 255.0f, 43.0f / 255.0f, 226.0f / 255.0f); this["brown"] = new Color(165.0f / 255.0f, 42.0f / 255.0f, 42.0f / 255.0f); this["burlywood"] = new Color(222.0f / 255.0f, 184.0f / 255.0f, 135.0f / 255.0f); this["cadetblue"] = new Color(95.0f / 255.0f, 158.0f / 255.0f, 160.0f / 255.0f); this["chartreuse"] = new Color(127.0f / 255.0f, 255.0f / 255.0f, 0.0f / 255.0f); this["chocolate"] = new Color(210.0f / 255.0f, 105.0f / 255.0f, 30.0f / 255.0f); this["coral"] = new Color(255.0f / 255.0f, 127.0f / 255.0f, 80.0f / 255.0f); this["cornflowerblue"] = new Color(100.0f / 255.0f, 149.0f / 255.0f, 237.0f / 255.0f); this["cornsilk"] = new Color(255.0f / 255.0f, 248.0f / 255.0f, 220.0f / 255.0f); this["crimson"] = new Color(220.0f / 255.0f, 20.0f / 255.0f, 60.0f / 255.0f); this["cyan"] = new Color(0.0f / 255.0f, 255.0f / 255.0f, 255.0f / 255.0f); this["darkblue"] = new Color(0.0f / 255.0f, 0.0f / 255.0f, 139.0f / 255.0f); this["darkcyan"] = new Color(0.0f / 255.0f, 139.0f / 255.0f, 139.0f / 255.0f); this["darkgoldenrod"] = new Color(184.0f / 255.0f, 134.0f / 255.0f, 11.0f / 255.0f); this["darkgray"] = new Color(169.0f / 255.0f, 169.0f / 255.0f, 169.0f / 255.0f); this["darkgrey"] = new Color(169.0f / 255.0f, 169.0f / 255.0f, 169.0f / 255.0f); this["darkgreen"] = new Color(0.0f / 255.0f, 100.0f / 255.0f, 0.0f / 255.0f); this["darkkhaki"] = new Color(189.0f / 255.0f, 183.0f / 255.0f, 107.0f / 255.0f); this["darkmagenta"] = new Color(139.0f / 255.0f, 0.0f / 255.0f, 139.0f / 255.0f); this["darkolivegreen"] = new Color(85.0f / 255.0f, 107.0f / 255.0f, 47.0f / 255.0f); this["darkorange"] = new Color(255.0f / 255.0f, 140.0f / 255.0f, 0.0f / 255.0f); this["darkorchid"] = new Color(153.0f / 255.0f, 50.0f / 255.0f, 204.0f / 255.0f); this["darkred"] = new Color(139.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f); this["darksalmon"] = new Color(233.0f / 255.0f, 150.0f / 255.0f, 122.0f / 255.0f); this["darkseagreen"] = new Color(143.0f / 255.0f, 188.0f / 255.0f, 143.0f / 255.0f); this["darkslateblue"] = new Color(72.0f / 255.0f, 61.0f / 255.0f, 139.0f / 255.0f); this["darkslategray"] = new Color(47.0f / 255.0f, 79.0f / 255.0f, 79.0f / 255.0f); this["darkslategrey"] = new Color(47.0f / 255.0f, 79.0f / 255.0f, 79.0f / 255.0f); this["darkturquoise"] = new Color(0.0f / 255.0f, 206.0f / 255.0f, 209.0f / 255.0f); this["darkviolet"] = new Color(148.0f / 255.0f, 0.0f / 255.0f, 211.0f / 255.0f); this["deeppink"] = new Color(255.0f / 255.0f, 20.0f / 255.0f, 147.0f / 255.0f); this["deepskyblue"] = new Color(0.0f / 255.0f, 191.0f / 255.0f, 255.0f / 255.0f); this["dimgray"] = new Color(105.0f / 255.0f, 105.0f / 255.0f, 105.0f / 255.0f); this["dimgrey"] = new Color(105.0f / 255.0f, 105.0f / 255.0f, 105.0f / 255.0f); this["dodgerblue"] = new Color(30.0f / 255.0f, 144.0f / 255.0f, 255.0f / 255.0f); this["firebrick"] = new Color(178.0f / 255.0f, 34.0f / 255.0f, 34.0f / 255.0f); this["floralwhite"] = new Color(255.0f / 255.0f, 250.0f / 255.0f, 240.0f / 255.0f); this["forestgreen"] = new Color(34.0f / 255.0f, 139.0f / 255.0f, 34.0f / 255.0f); this["fuchsia"] = new Color(255.0f / 255.0f, 0.0f / 255.0f, 255.0f / 255.0f); this["gainsboro"] = new Color(220.0f / 255.0f, 220.0f / 255.0f, 220.0f / 255.0f); this["ghostwhite"] = new Color(248.0f / 255.0f, 248.0f / 255.0f, 255.0f / 255.0f); this["gold"] = new Color(255.0f / 255.0f, 215.0f / 255.0f, 0.0f / 255.0f); this["goldenrod"] = new Color(218.0f / 255.0f, 165.0f / 255.0f, 32.0f / 255.0f); this["gray"] = new Color(128.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f); this["grey"] = new Color(128.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f); this["green"] = new Color(0.0f / 255.0f, 128.0f / 255.0f, 0.0f / 255.0f); this["greenyellow"] = new Color(173.0f / 255.0f, 255.0f / 255.0f, 47.0f / 255.0f); this["honeydew"] = new Color(240.0f / 255.0f, 255.0f / 255.0f, 240.0f / 255.0f); this["hotpink"] = new Color(255.0f / 255.0f, 105.0f / 255.0f, 180.0f / 255.0f); this["indianred"] = new Color(205.0f / 255.0f, 92.0f / 255.0f, 92.0f / 255.0f); this["indigo"] = new Color(75.0f / 255.0f, 0.0f / 255.0f, 130.0f / 255.0f); this["ivory"] = new Color(255.0f / 255.0f, 255.0f / 255.0f, 240.0f / 255.0f); this["khaki"] = new Color(240.0f / 255.0f, 230.0f / 255.0f, 140.0f / 255.0f); this["lavender"] = new Color(230.0f / 255.0f, 230.0f / 255.0f, 250.0f / 255.0f); this["lavenderblush"] = new Color(255.0f / 255.0f, 240.0f / 255.0f, 245.0f / 255.0f); this["lawngreen"] = new Color(124.0f / 255.0f, 252.0f / 255.0f, 0.0f / 255.0f); this["lemonchiffon"] = new Color(255.0f / 255.0f, 250.0f / 255.0f, 205.0f / 255.0f); this["lightblue"] = new Color(173.0f / 255.0f, 216.0f / 255.0f, 230.0f / 255.0f); this["lightcoral"] = new Color(240.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f); this["lightcyan"] = new Color(224.0f / 255.0f, 255.0f / 255.0f, 255.0f / 255.0f); this["lightgoldenrodyellow"] = new Color(250.0f / 255.0f, 250.0f / 255.0f, 210.0f / 255.0f); this["lightgray"] = new Color(211.0f / 255.0f, 211.0f / 255.0f, 211.0f / 255.0f); this["lightgrey"] = new Color(211.0f / 255.0f, 211.0f / 255.0f, 211.0f / 255.0f); this["lightgreen"] = new Color(144.0f / 255.0f, 238.0f / 255.0f, 144.0f / 255.0f); this["lightpink"] = new Color(255.0f / 255.0f, 182.0f / 255.0f, 193.0f / 255.0f); this["lightsalmon"] = new Color(255.0f / 255.0f, 160.0f / 255.0f, 122.0f / 255.0f); this["lightseagreen"] = new Color(32.0f / 255.0f, 178.0f / 255.0f, 170.0f / 255.0f); this["lightskyblue"] = new Color(135.0f / 255.0f, 206.0f / 255.0f, 250.0f / 255.0f); this["lightslategray"] = new Color(119.0f / 255.0f, 136.0f / 255.0f, 153.0f / 255.0f); this["lightslategrey"] = new Color(119.0f / 255.0f, 136.0f / 255.0f, 153.0f / 255.0f); this["lightsteelblue"] = new Color(176.0f / 255.0f, 196.0f / 255.0f, 222.0f / 255.0f); this["lightyellow"] = new Color(255.0f / 255.0f, 255.0f / 255.0f, 224.0f / 255.0f); this["lime"] = new Color(0.0f / 255.0f, 255.0f / 255.0f, 0.0f / 255.0f); this["limegreen"] = new Color(50.0f / 255.0f, 205.0f / 255.0f, 50.0f / 255.0f); this["linen"] = new Color(250.0f / 255.0f, 240.0f / 255.0f, 230.0f / 255.0f); this["magenta"] = new Color(255.0f / 255.0f, 0.0f / 255.0f, 255.0f / 255.0f); this["maroon"] = new Color(128.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f); this["mediumaquamarine"] = new Color(102.0f / 255.0f, 205.0f / 255.0f, 170.0f / 255.0f); this["mediumblue"] = new Color(0.0f / 255.0f, 0.0f / 255.0f, 205.0f / 255.0f); this["mediumorchid"] = new Color(186.0f / 255.0f, 85.0f / 255.0f, 211.0f / 255.0f); this["mediumpurple"] = new Color(147.0f / 255.0f, 112.0f / 255.0f, 219.0f / 255.0f); this["mediumseagreen"] = new Color(60.0f / 255.0f, 179.0f / 255.0f, 113.0f / 255.0f); this["mediumslateblue"] = new Color(123.0f / 255.0f, 104.0f / 255.0f, 238.0f / 255.0f); this["mediumspringgreen"] = new Color(0.0f / 255.0f, 250.0f / 255.0f, 154.0f / 255.0f); this["mediumturquoise"] = new Color(72.0f / 255.0f, 209.0f / 255.0f, 204.0f / 255.0f); this["mediumvioletred"] = new Color(199.0f / 255.0f, 21.0f / 255.0f, 133.0f / 255.0f); this["midnightblue"] = new Color(25.0f / 255.0f, 25.0f / 255.0f, 112.0f / 255.0f); this["mintcream"] = new Color(245.0f / 255.0f, 255.0f / 255.0f, 250.0f / 255.0f); this["mistyrose"] = new Color(255.0f / 255.0f, 228.0f / 255.0f, 225.0f / 255.0f); this["moccasin"] = new Color(255.0f / 255.0f, 228.0f / 255.0f, 181.0f / 255.0f); this["navajowhite"] = new Color(255.0f / 255.0f, 222.0f / 255.0f, 173.0f / 255.0f); this["navy"] = new Color(0.0f / 255.0f, 0.0f / 255.0f, 128.0f / 255.0f); this["oldlace"] = new Color(253.0f / 255.0f, 245.0f / 255.0f, 230.0f / 255.0f); this["olive"] = new Color(128.0f / 255.0f, 128.0f / 255.0f, 0.0f / 255.0f); this["olivedrab"] = new Color(107.0f / 255.0f, 142.0f / 255.0f, 35.0f / 255.0f); this["orange"] = new Color(255.0f / 255.0f, 165.0f / 255.0f, 0.0f / 255.0f); this["orangered"] = new Color(255.0f / 255.0f, 69.0f / 255.0f, 0.0f / 255.0f); this["orchid"] = new Color(218.0f / 255.0f, 112.0f / 255.0f, 214.0f / 255.0f); this["palegoldenrod"] = new Color(238.0f / 255.0f, 232.0f / 255.0f, 170.0f / 255.0f); this["palegreen"] = new Color(152.0f / 255.0f, 251.0f / 255.0f, 152.0f / 255.0f); this["paleturquoise"] = new Color(175.0f / 255.0f, 238.0f / 255.0f, 238.0f / 255.0f); this["palevioletred"] = new Color(219.0f / 255.0f, 112.0f / 255.0f, 147.0f / 255.0f); this["papayawhip"] = new Color(255.0f / 255.0f, 239.0f / 255.0f, 213.0f / 255.0f); this["peachpuff"] = new Color(255.0f / 255.0f, 218.0f / 255.0f, 185.0f / 255.0f); this["peru"] = new Color(205.0f / 255.0f, 133.0f / 255.0f, 63.0f / 255.0f); this["pink"] = new Color(255.0f / 255.0f, 192.0f / 255.0f, 203.0f / 255.0f); this["plum"] = new Color(221.0f / 255.0f, 160.0f / 255.0f, 221.0f / 255.0f); this["powderblue"] = new Color(176.0f / 255.0f, 224.0f / 255.0f, 230.0f / 255.0f); this["purple"] = new Color(128.0f / 255.0f, 0.0f / 255.0f, 128.0f / 255.0f); this["rebeccapurple"] = new Color(102.0f / 255.0f, 51.0f / 255.0f, 153.0f / 255.0f); this["red"] = new Color(255.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f); this["rosybrown"] = new Color(188.0f / 255.0f, 143.0f / 255.0f, 143.0f / 255.0f); this["royalblue"] = new Color(65.0f / 255.0f, 105.0f / 255.0f, 225.0f / 255.0f); this["saddlebrown"] = new Color(139.0f / 255.0f, 69.0f / 255.0f, 19.0f / 255.0f); this["salmon"] = new Color(250.0f / 255.0f, 128.0f / 255.0f, 114.0f / 255.0f); this["sandybrown"] = new Color(244.0f / 255.0f, 164.0f / 255.0f, 96.0f / 255.0f); this["seagreen"] = new Color(46.0f / 255.0f, 139.0f / 255.0f, 87.0f / 255.0f); this["seashell"] = new Color(255.0f / 255.0f, 245.0f / 255.0f, 238.0f / 255.0f); this["sienna"] = new Color(160.0f / 255.0f, 82.0f / 255.0f, 45.0f / 255.0f); this["silver"] = new Color(192.0f / 255.0f, 192.0f / 255.0f, 192.0f / 255.0f); this["skyblue"] = new Color(135.0f / 255.0f, 206.0f / 255.0f, 235.0f / 255.0f); this["slateblue"] = new Color(106.0f / 255.0f, 90.0f / 255.0f, 205.0f / 255.0f); this["slategray"] = new Color(112.0f / 255.0f, 128.0f / 255.0f, 144.0f / 255.0f); this["slategrey"] = new Color(112.0f / 255.0f, 128.0f / 255.0f, 144.0f / 255.0f); this["snow"] = new Color(255.0f / 255.0f, 250.0f / 255.0f, 250.0f / 255.0f); this["springgreen"] = new Color(0.0f / 255.0f, 255.0f / 255.0f, 127.0f / 255.0f); this["steelblue"] = new Color(70.0f / 255.0f, 130.0f / 255.0f, 180.0f / 255.0f); this["tan"] = new Color(210.0f / 255.0f, 180.0f / 255.0f, 140.0f / 255.0f); this["teal"] = new Color(0.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f); this["thistle"] = new Color(216.0f / 255.0f, 191.0f / 255.0f, 216.0f / 255.0f); this["tomato"] = new Color(255.0f / 255.0f, 99.0f / 255.0f, 71.0f / 255.0f); this["turquoise"] = new Color(64.0f / 255.0f, 224.0f / 255.0f, 208.0f / 255.0f); this["violet"] = new Color(238.0f / 255.0f, 130.0f / 255.0f, 238.0f / 255.0f); this["wheat"] = new Color(245.0f / 255.0f, 222.0f / 255.0f, 179.0f / 255.0f); this["white"] = new Color(255.0f / 255.0f, 255.0f / 255.0f, 255.0f / 255.0f); this["whitesmoke"] = new Color(245.0f / 255.0f, 245.0f / 255.0f, 245.0f / 255.0f); this["yellow"] = new Color(255.0f / 255.0f, 255.0f / 255.0f, 0.0f / 255.0f); this["yellowgreen"] = new Color(154.0f / 255.0f, 205.0f / 255.0f, 50.0f / 255.0f); } } // The boring NamedWebColorDictionary class } // namespace