3335 lines
145 KiB
C#
3335 lines
145 KiB
C#
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
|
|
{
|
|
/// <summary>An enum describing the viewport options to use when importing the SVG document.</summary>
|
|
public enum ViewportOptions
|
|
{
|
|
/// <summary>Don't preserve the viewport defined in the SVG document.</summary>
|
|
DontPreserve,
|
|
|
|
/// <summary>Preserves the viewport defined in the SVG document.</summary>
|
|
PreserveViewport,
|
|
|
|
/// <summary>Applies the root view-box defined in the SVG document (if any).</summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
OnlyApplyRootViewBox
|
|
}
|
|
|
|
/// <summary>Reads an SVG document and builds a vector scene.</summary>
|
|
public class SVGParser
|
|
{
|
|
/// <summary>A structure containing the SVG scene data.</summary>
|
|
public struct SceneInfo
|
|
{
|
|
internal SceneInfo(Scene scene, Rect sceneViewport, Dictionary<SceneNode, float> nodeOpacities, Dictionary<string, SceneNode> nodeIDs)
|
|
{
|
|
Scene = scene;
|
|
SceneViewport = sceneViewport;
|
|
NodeOpacity = nodeOpacities;
|
|
NodeIDs = nodeIDs;
|
|
}
|
|
|
|
/// <summary>The vector scene.</summary>
|
|
public Scene Scene { get; }
|
|
|
|
/// <summary>The position and size of the SVG document</summary>
|
|
public Rect SceneViewport { get; }
|
|
|
|
/// <summary>A dictionary containing the opacity of the scene nodes.</summary>
|
|
public Dictionary<SceneNode, float> NodeOpacity { get; }
|
|
|
|
/// <summary>A dictionary containing the scene node for a given ID</summary>
|
|
public Dictionary<string, SceneNode> NodeIDs { get; }
|
|
}
|
|
|
|
/// <summary>Kicks off an SVG file import.</summary>
|
|
/// <param name="textReader">The reader object containing the SVG file data</param>
|
|
/// <param name="dpi">The DPI of the SVG file, or 0 to use the device's DPI</param>
|
|
/// <param name="pixelsPerUnit">How many SVG units fit in a Unity unit</param>
|
|
/// <param name="windowWidth">The default with of the viewport, may be 0</param>
|
|
/// <param name="windowHeight">The default height of the viewport, may be 0</param>
|
|
/// <param name="clipViewport">Whether the vector scene should be clipped by the SVG document's viewport</param>
|
|
/// <returns>A SceneInfo object containing the scene data</returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Kicks off an SVG file import.</summary>
|
|
/// <param name="textReader">The reader object containing the SVG file data</param>
|
|
/// <param name="viewportOptions">The viewport options to use</param>
|
|
/// <param name="dpi">The DPI of the SVG file, or 0 to use the device's DPI</param>
|
|
/// <param name="pixelsPerUnit">How many SVG units fit in a Unity unit</param>
|
|
/// <param name="windowWidth">The default with of the viewport, may be 0</param>
|
|
/// <param name="windowHeight">The default height of the viewport, may be 0</param>
|
|
/// <returns>A SceneInfo object containing the scene data</returns>
|
|
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<SceneNode, float> nodeOpacities;
|
|
Dictionary<string, SceneNode> 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<SceneNode> { scene.Root },
|
|
Clipper = new SceneNode() { Shapes = new List<Shape>() { 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<string, object> {}
|
|
internal class SVGPostponedFills : Dictionary<IFill, string> { }
|
|
|
|
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<ElemHandler>(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<SceneNode, float> NodeOpacities { get { return nodeOpacity; } }
|
|
public Dictionary<string, SceneNode> 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<SceneNode>();
|
|
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<Shape>(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<Shape>(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<Texture2D>("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 <image> (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<Shape>(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<NodeReferenceData> 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<Shape>(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>());
|
|
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<Shape>(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<BezierPathSegment>(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<Shape>(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<BezierPathSegment>(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<Shape>(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>());
|
|
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<PostponedClip> 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<Shape>(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<NodeReferenceData> 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<NodeReferenceData> refList;
|
|
if (!postponedSymbolData.TryGetValue(iri, out refList))
|
|
{
|
|
refList = new List<NodeReferenceData>();
|
|
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 <use> 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<SceneNode>();
|
|
|
|
SVGStyleResolver.StyleLayer rootLayer = null;
|
|
if (isDeferred)
|
|
{
|
|
// If deferred, push back the original <use> 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<SceneNode>(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<SceneNode> children = null;
|
|
if (node.Children != null)
|
|
{
|
|
children = new List<SceneNode>(node.Children.Count);
|
|
foreach (var c in node.Children)
|
|
children.Add(CloneSceneNode(c));
|
|
}
|
|
|
|
List<Shape> shapes = null;
|
|
if (node.Shapes != null)
|
|
{
|
|
shapes = new List<Shape>(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 <image> 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 <use> 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<PostponedClip> clips;
|
|
if (!postponedClip.TryGetValue(reference, out clips))
|
|
clips = new List<PostponedClip>(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<SceneNode> { 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<SceneNode> { 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<HierarchyUpdate>();
|
|
|
|
// 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<SceneNode>(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<SceneNode> { 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<SceneNode>(20)
|
|
};
|
|
|
|
var fill = new SceneNode() {
|
|
Transform = Matrix2D.identity,
|
|
Children = new List<SceneNode> { 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<Shape> { 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<SceneNode> { 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<string, ElemHandler>
|
|
{
|
|
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<string, Handlers> subTags = new Dictionary<string, Handlers>(); // For each element, the set of elements supported as its children
|
|
Dictionary<GradientFill, GradientExData> gradientExInfo = new Dictionary<GradientFill, GradientExData>();
|
|
Dictionary<SceneNode, ViewBoxInfo> symbolViewBoxes = new Dictionary<SceneNode, ViewBoxInfo>();
|
|
Dictionary<SceneNode, NodeGlobalSceneState> nodeGlobalSceneState = new Dictionary<SceneNode, NodeGlobalSceneState>();
|
|
Dictionary<SceneNode, float> nodeOpacity = new Dictionary<SceneNode, float>();
|
|
Dictionary<string, SceneNode> nodeIDs = new Dictionary<string, SceneNode>();
|
|
Dictionary<SceneNode, SVGStyleResolver.StyleLayer> nodeStyleLayers = new Dictionary<SceneNode, SVGStyleResolver.StyleLayer>();
|
|
Dictionary<SceneNode, ClipData> clipData = new Dictionary<SceneNode, ClipData>();
|
|
Dictionary<SceneNode, PatternData> patternData = new Dictionary<SceneNode, PatternData>();
|
|
Dictionary<SceneNode, MaskData> maskData = new Dictionary<SceneNode, MaskData>();
|
|
Dictionary<string, List<NodeReferenceData>> postponedSymbolData = new Dictionary<string, List<NodeReferenceData>>();
|
|
Dictionary<string, List<PostponedStopData>> postponedStopData = new Dictionary<string, List<PostponedStopData>>();
|
|
Dictionary<string, List<PostponedClip>> postponedClip = new Dictionary<string, List<PostponedClip>>();
|
|
SVGPostponedFills postponedFills = new SVGPostponedFills();
|
|
List<NodeWithParent> invisibleNodes = new List<NodeWithParent>();
|
|
Stack<Vector2> currentContainerSize = new Stack<Vector2>();
|
|
Stack<Vector2> currentViewBoxSize = new Stack<Vector2>();
|
|
Stack<SceneNode> currentSceneNode = new Stack<SceneNode>();
|
|
GradientFill currentGradientFill;
|
|
string currentGradientId;
|
|
string currentGradientLink;
|
|
ElemHandler[] allElems;
|
|
HashSet<ElemHandler> 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<string>();
|
|
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<string> SortedClasses(List<string> classes)
|
|
{
|
|
// We may not have parsed the global sheets yet (happens when setting a class on the root <svg> 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<string> classes;
|
|
public string id;
|
|
}
|
|
|
|
public class StyleLayer
|
|
{
|
|
public SVGStyleSheet styleSheet;
|
|
public SVGPropertySheet attributeSheet;
|
|
public NodeData nodeData;
|
|
}
|
|
|
|
private List<StyleLayer> layers = new List<StyleLayer>();
|
|
private SVGStyleSheet globalStyleSheet = new SVGStyleSheet();
|
|
private Dictionary<SceneNode, StyleLayer> nodeLayers = new Dictionary<SceneNode, StyleLayer>();
|
|
}
|
|
|
|
internal class SVGAttribParser
|
|
{
|
|
public static List<BezierContour> 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<BezierSegment> currentContour = new LinkedList<BezierSegment>();
|
|
List<BezierContour> contours = new List<BezierContour>();
|
|
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<string, Color>
|
|
{
|
|
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
|