From 3f4ee3330917fed60ae81d4640bab037bb01959e Mon Sep 17 00:00:00 2001 From: Eric Domke Date: Sat, 23 Aug 2014 11:44:44 -0400 Subject: [PATCH] Gradient, Pattern, and Clip Fixes - Better parser for dealing with parsing edge cases - Fix corner radius for rectangles - Fix to gradients. Most tests now pass - Add the possibility for a fallback paint server - Initial fixes to clipping - Start marking passed test - Fixes to pattern rendering --- Samples/SVGViewer/SvgViewer.cs | 5 +- Source/Basic Shapes/SvgPolygon.cs | 8 +- Source/Basic Shapes/SvgPolyline.cs | 2 +- Source/Basic Shapes/SvgRectangle.cs | 4 +- Source/Basic Shapes/SvgVisualElement.cs | 57 ++- Source/Basic Shapes/SvgVisualElementStyle.cs | 12 + Source/Clipping and Masking/SvgClipPath.cs | 17 +- Source/DataTypes/SvgCoordinateUnits.cs | 3 + Source/DataTypes/SvgOverflow.cs | 4 +- Source/DataTypes/SvgPointCollection.cs | 86 +++++ Source/DataTypes/SvgUnit.cs | 27 +- Source/DataTypes/SvgUnitConverter.cs | 2 +- Source/DataTypes/SvgViewBox.cs | 12 +- Source/Document Structure/SvgFragment.cs | 28 ++ Source/Document Structure/SvgUse.cs | 2 +- Source/Painting/EnumConverters.cs | 17 + Source/Painting/SvgColourServer.cs | 4 +- Source/Painting/SvgDeferredPaintServer.cs | 5 +- Source/Painting/SvgFallbackPaintServer .cs | 49 +++ Source/Painting/SvgGradientServer.cs | 41 +-- Source/Painting/SvgGradientStop.cs | 36 +- Source/Painting/SvgLinearGradientServer.cs | 343 ++++++++++++++----- Source/Painting/SvgPaintServer.cs | 4 +- Source/Painting/SvgPaintServerFactory.cs | 85 +++-- Source/Painting/SvgPatternServer.cs | 156 ++++++--- Source/Painting/SvgRadialGradientServer.cs | 172 ++++++++-- Source/Paths/CoordinateParser.cs | 332 ++++++++++++++++++ Source/Paths/SvgPath.cs | 66 ++-- Source/Paths/SvgPathBuilder.cs | 317 +---------------- Source/Svg.csproj | 3 + Source/SvgAttributeCollection.cs | 4 +- Source/SvgDocument.cs | 2 +- Source/SvgElementIdManager.cs | 1 + Tests/SvgW3CTestRunner/View.cs | 10 +- Tests/W3CTestSuite/PassingTests.txt | 159 +++++++++ 35 files changed, 1419 insertions(+), 656 deletions(-) create mode 100644 Source/DataTypes/SvgPointCollection.cs create mode 100644 Source/Painting/SvgFallbackPaintServer .cs create mode 100644 Source/Paths/CoordinateParser.cs create mode 100644 Tests/W3CTestSuite/PassingTests.txt diff --git a/Samples/SVGViewer/SvgViewer.cs b/Samples/SVGViewer/SvgViewer.cs index ab5d75f..4f44d80 100644 --- a/Samples/SVGViewer/SvgViewer.cs +++ b/Samples/SVGViewer/SvgViewer.cs @@ -43,9 +43,10 @@ namespace SVGViewer private void RenderSvg(SvgDocument svgDoc) { - var render = new DebugRenderer(); - svgDoc.Draw(render); + //var render = new DebugRenderer(); + //svgDoc.Draw(render); svgImage.Image = svgDoc.Draw(); + svgImage.Image.Save(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(svgDoc.BaseUri.LocalPath), "output.png")); } } } diff --git a/Source/Basic Shapes/SvgPolygon.cs b/Source/Basic Shapes/SvgPolygon.cs index b1c3e22..4c46a1a 100644 --- a/Source/Basic Shapes/SvgPolygon.cs +++ b/Source/Basic Shapes/SvgPolygon.cs @@ -15,13 +15,13 @@ namespace Svg public class SvgPolygon : SvgVisualElement { private GraphicsPath _path; - private SvgUnitCollection _points; + private SvgPointCollection _points; /// /// The points that make up the SvgPolygon /// [SvgAttribute("points")] - public SvgUnitCollection Points + public SvgPointCollection Points { get { return this._points; } set { this._points = value; this.IsPathDirty = true; } @@ -41,7 +41,7 @@ namespace Svg try { - for (int i = 2; i < this._points.Count; i+=2) + for (int i = 2; (i + 1) < this._points.Count; i += 2) { var endPoint = SvgUnit.GetDevicePoint(this._points[i], this._points[i+1], renderer, this); @@ -81,7 +81,7 @@ namespace Svg public override SvgElement DeepCopy() { var newObj = base.DeepCopy() as SvgPolygon; - newObj.Points = new SvgUnitCollection(); + newObj.Points = new SvgPointCollection(); foreach (var pt in this.Points) newObj.Points.Add(pt); return newObj; diff --git a/Source/Basic Shapes/SvgPolyline.cs b/Source/Basic Shapes/SvgPolyline.cs index 2b262f7..e6912fe 100644 --- a/Source/Basic Shapes/SvgPolyline.cs +++ b/Source/Basic Shapes/SvgPolyline.cs @@ -22,7 +22,7 @@ namespace Svg try { - for (int i = 0; i < Points.Count; i += 2) + for (int i = 0; (i + 1) < Points.Count; i += 2) { PointF endPoint = new PointF(Points[i].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this), Points[i + 1].ToDeviceValue(renderer, UnitRenderingType.Vertical, this)); diff --git a/Source/Basic Shapes/SvgRectangle.cs b/Source/Basic Shapes/SvgRectangle.cs index c9d79c3..c468556 100644 --- a/Source/Basic Shapes/SvgRectangle.cs +++ b/Source/Basic Shapes/SvgRectangle.cs @@ -197,8 +197,8 @@ namespace Svg var lineEnd = new PointF(); var width = Width.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this); var height = Height.ToDeviceValue(renderer, UnitRenderingType.Vertical, this); - var rx = CornerRadiusX.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this) * 2; - var ry = CornerRadiusY.ToDeviceValue(renderer, UnitRenderingType.Vertical, this) * 2; + var rx = Math.Min(CornerRadiusX.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this) * 2, width); + var ry = Math.Min(CornerRadiusY.ToDeviceValue(renderer, UnitRenderingType.Vertical, this) * 2, height); var location = Location.ToDeviceValue(renderer, this); // Start diff --git a/Source/Basic Shapes/SvgVisualElement.cs b/Source/Basic Shapes/SvgVisualElement.cs index 99b14f3..e30876b 100644 --- a/Source/Basic Shapes/SvgVisualElement.cs +++ b/Source/Basic Shapes/SvgVisualElement.cs @@ -2,6 +2,7 @@ using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Diagnostics; +using System.Linq; namespace Svg { @@ -53,13 +54,23 @@ namespace Svg set { this._dirty = value; } } + /// + /// Gets the associated if one has been specified. + /// + [SvgAttribute("clip")] + public virtual string Clip + { + get { return this.Attributes.GetInheritedAttribute("clip"); } + set { this.Attributes["clip"] = value; } + } + /// /// Gets the associated if one has been specified. /// [SvgAttribute("clip-path")] public virtual Uri ClipPath { - get { return this.Attributes.GetAttribute("clip-path"); } + get { return this.Attributes.GetInheritedAttribute("clip-path"); } set { this.Attributes["clip-path"] = value; } } @@ -79,7 +90,7 @@ namespace Svg [SvgAttribute("filter")] public virtual Uri Filter { - get { return this.Attributes.GetAttribute("filter"); } + get { return this.Attributes.GetInheritedAttribute("filter"); } set { this.Attributes["filter"] = value; } } @@ -180,7 +191,7 @@ namespace Svg { if (this.Fill != null) { - using (Brush brush = this.Fill.GetBrush(this, renderer, Math.Min(Math.Max(this.FillOpacity * this.Opacity, 0), 1))) + using (var brush = this.Fill.GetBrush(this, renderer, Math.Min(Math.Max(this.FillOpacity * this.Opacity, 0), 1))) { if (brush != null) { @@ -200,15 +211,22 @@ namespace Svg if (this.Stroke != null && this.Stroke != SvgColourServer.None) { float strokeWidth = this.StrokeWidth.ToDeviceValue(renderer, UnitRenderingType.Other, this); - using (var pen = new Pen(this.Stroke.GetBrush(this, renderer, Math.Min(Math.Max(this.StrokeOpacity * this.Opacity, 0), 1)), strokeWidth)) + using (var brush = this.Stroke.GetBrush(this, renderer, Math.Min(Math.Max(this.StrokeOpacity * this.Opacity, 0), 1), true)) { - if (this.StrokeDashArray != null && this.StrokeDashArray.Count > 0) + if (brush != null) { - /* divide by stroke width - GDI behaviour that I don't quite understand yet.*/ - pen.DashPattern = this.StrokeDashArray.ConvertAll(u => ((u.Value <= 0) ? 1 : u.Value) / ((strokeWidth <= 0) ? 1 : strokeWidth)).ToArray(); + using (var pen = new Pen(brush, strokeWidth)) + { + if (this.StrokeDashArray != null && this.StrokeDashArray.Count > 0) + { + /* divide by stroke width - GDI behaviour that I don't quite understand yet.*/ + pen.DashPattern = this.StrokeDashArray.ConvertAll(u => ((u.ToDeviceValue(renderer, UnitRenderingType.Other, this) <= 0) ? 1 : u.ToDeviceValue(renderer, UnitRenderingType.Other, this)) / + ((strokeWidth <= 0) ? 1 : strokeWidth)).ToArray(); + } + + renderer.DrawPath(pen, this.Path(renderer)); + } } - - renderer.DrawPath(pen, this.Path(renderer)); } } } @@ -219,14 +237,27 @@ namespace Svg /// The to have its clipping region set. protected internal virtual void SetClip(ISvgRenderer renderer) { - if (this.ClipPath != null) + if (this.ClipPath != null || !string.IsNullOrEmpty(this.Clip)) { - SvgClipPath clipPath = this.OwnerDocument.GetElementById(this.ClipPath.ToString()); this._previousClip = renderer.GetClip(); - if (clipPath != null) + if (this.ClipPath != null) + { + SvgClipPath clipPath = this.OwnerDocument.GetElementById(this.ClipPath.ToString()); + if (clipPath != null) renderer.SetClip(clipPath.GetClipRegion(this), CombineMode.Intersect); + } + + var clip = this.Clip; + if (!string.IsNullOrEmpty(clip) && clip.StartsWith("rect(")) { - renderer.SetClip(clipPath.GetClipRegion(this), CombineMode.Intersect); + clip = clip.Trim(); + var offsets = (from o in clip.Substring(5, clip.Length - 6).Split(',') + select float.Parse(o.Trim())).ToList(); + var bounds = this.Bounds; + var clipRect = new RectangleF(bounds.Left + offsets[3], bounds.Top + offsets[0], + bounds.Width - (offsets[3] + offsets[1]), + bounds.Height - (offsets[2] + offsets[0])); + renderer.SetClip(new Region(clipRect), CombineMode.Intersect); } } } diff --git a/Source/Basic Shapes/SvgVisualElementStyle.cs b/Source/Basic Shapes/SvgVisualElementStyle.cs index f2e08df..9d6ba60 100644 --- a/Source/Basic Shapes/SvgVisualElementStyle.cs +++ b/Source/Basic Shapes/SvgVisualElementStyle.cs @@ -156,6 +156,18 @@ namespace Svg set { this.Attributes["stroke-opacity"] = FixOpacityValue(value); } } + /// + /// Gets or sets the colour of the gradient stop. + /// + /// Apparently this can be set on non-sensical elements. Don't ask; just check the tests. + [SvgAttribute("stop-color")] + [TypeConverter(typeof(SvgPaintServerFactory))] + public SvgPaintServer StopColor + { + get { return this.Attributes["stop-color"] as SvgPaintServer; } + set { this.Attributes["stop-color"] = value; } + } + /// /// Gets or sets the opacity of the element. 1.0 is fully opaque; 0.0 is transparent. /// diff --git a/Source/Clipping and Masking/SvgClipPath.cs b/Source/Clipping and Masking/SvgClipPath.cs index feed3d3..fdc805d 100644 --- a/Source/Clipping and Masking/SvgClipPath.cs +++ b/Source/Clipping and Masking/SvgClipPath.cs @@ -26,7 +26,7 @@ namespace Svg /// public SvgClipPath() { - this.ClipPathUnits = SvgCoordinateUnits.ObjectBoundingBox; + this.ClipPathUnits = SvgCoordinateUnits.Inherit; } private GraphicsPath cachedClipPath = null; @@ -49,7 +49,20 @@ namespace Svg this._pathDirty = false; } - return new Region(cachedClipPath); + var result = cachedClipPath; + if (ClipPathUnits == SvgCoordinateUnits.ObjectBoundingBox) + { + result = (GraphicsPath)cachedClipPath.Clone(); + using (var transform = new Matrix()) + { + var bounds = owner.Bounds; + transform.Scale(bounds.Width, bounds.Height, MatrixOrder.Append); + transform.Translate(bounds.Left, bounds.Top, MatrixOrder.Append); + result.Transform(transform); + } + } + + return new Region(result); } /// diff --git a/Source/DataTypes/SvgCoordinateUnits.cs b/Source/DataTypes/SvgCoordinateUnits.cs index 9fdafdb..a17c83e 100644 --- a/Source/DataTypes/SvgCoordinateUnits.cs +++ b/Source/DataTypes/SvgCoordinateUnits.cs @@ -2,14 +2,17 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.ComponentModel; namespace Svg { /// /// Defines the various coordinate units certain SVG elements may use. /// + [TypeConverter(typeof(SvgCoordinateUnitsConverter))] public enum SvgCoordinateUnits { + Inherit, /// /// Indicates that the coordinate system of the owner element is to be used. /// diff --git a/Source/DataTypes/SvgOverflow.cs b/Source/DataTypes/SvgOverflow.cs index 97bddcc..1d7ae83 100644 --- a/Source/DataTypes/SvgOverflow.cs +++ b/Source/DataTypes/SvgOverflow.cs @@ -7,10 +7,10 @@ namespace Svg { public enum SvgOverflow { + inherit, auto, visible, hidden, - scroll, - inherit + scroll } } diff --git a/Source/DataTypes/SvgPointCollection.cs b/Source/DataTypes/SvgPointCollection.cs new file mode 100644 index 0000000..4459e46 --- /dev/null +++ b/Source/DataTypes/SvgPointCollection.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel; +using System.Globalization; + +namespace Svg +{ + /// + /// Represents a list of used with the and . + /// + [TypeConverter(typeof(SvgPointCollectionConverter))] + public class SvgPointCollection : List + { + public override string ToString() + { + string ret = ""; + foreach (var unit in this) + { + ret += unit.ToString() + " "; + } + + return ret; + } + } + + /// + /// A class to convert string into instances. + /// + internal class SvgPointCollectionConverter : TypeConverter + { + //private static readonly SvgUnitConverter _unitConverter = new SvgUnitConverter(); + + + /// + /// Converts the given object to the type of this converter, using the specified context and culture information. + /// + /// An that provides a format context. + /// The to use as the current culture. + /// The to convert. + /// + /// An that represents the converted value. + /// + /// The conversion cannot be performed. + public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) + { + if (value is string) + { + var strValue = ((string)value).Trim(); + if (string.Compare(strValue, "none", StringComparison.InvariantCultureIgnoreCase) == 0) return null; + + var parser = new CoordinateParser(strValue); + var pointValue = 0.0f; + var result = new SvgPointCollection(); + while (parser.TryGetFloat(out pointValue)) + { + result.Add(new SvgUnit(SvgUnitType.User, pointValue)); + } + + return result; + } + + return base.ConvertFrom(context, culture, value); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + if (destinationType == typeof(string)) + { + return true; + } + return base.CanConvertTo(context, destinationType); + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (destinationType == typeof(string)) + { + return ((SvgPointCollection)value).ToString(); + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} diff --git a/Source/DataTypes/SvgUnit.cs b/Source/DataTypes/SvgUnit.cs index 524b1df..f5a8f12 100644 --- a/Source/DataTypes/SvgUnit.cs +++ b/Source/DataTypes/SvgUnit.cs @@ -20,7 +20,7 @@ namespace Svg /// /// Gets and empty . /// - public static readonly SvgUnit Empty = new SvgUnit(SvgUnitType.User, 0); + public static readonly SvgUnit Empty = new SvgUnit(SvgUnitType.User, 0) { _isEmpty = true }; /// /// Gets an with a value of none. @@ -86,26 +86,7 @@ namespace Svg var type = this.Type; var value = this.Value; - - // Deal with fractional pattern units - var coordElem = owner as ISvgSupportsCoordinateUnits; - if (coordElem != null && coordElem.GetUnits() == SvgCoordinateUnits.ObjectBoundingBox && type != SvgUnitType.Percentage) - { - type = SvgUnitType.Percentage; - value *= 100; - } - - var element = owner as SvgElement; - if (element != null) - { - var pattern = element.Parents.OfType().FirstOrDefault(); - if (pattern != null && pattern.PatternContentUnits == SvgCoordinateUnits.ObjectBoundingBox && type != SvgUnitType.Percentage) - { - type = SvgUnitType.Percentage; - value *= 100; - } - } - + float points; switch (type) @@ -320,9 +301,9 @@ namespace Svg /// The value. public SvgUnit(SvgUnitType type, float value) { + this._isEmpty = false; this._type = type; this._value = value; - this._isEmpty = (this._value == 0.0f); this._deviceValue = null; } @@ -332,9 +313,9 @@ namespace Svg /// The value. public SvgUnit(float value) { + this._isEmpty = false; this._value = value; this._type = SvgUnitType.User; - this._isEmpty = (this._value == 0.0f); this._deviceValue = null; } diff --git a/Source/DataTypes/SvgUnitConverter.cs b/Source/DataTypes/SvgUnitConverter.cs index 4787fab..63e403d 100644 --- a/Source/DataTypes/SvgUnitConverter.cs +++ b/Source/DataTypes/SvgUnitConverter.cs @@ -35,7 +35,7 @@ namespace Svg for (int i = 0; i < unit.Length; i++) { // If the character is a percent sign or a letter which is not an exponent 'e' - if (unit[i] == '%' || (char.IsLetter(unit[i]) && !(unit[i] == 'e' && i < unit.Length - 1 && !char.IsLetter(unit[i + 1])))) + if (unit[i] == '%' || (char.IsLetter(unit[i]) && !((unit[i] == 'e' || unit[i] == 'E') && i < unit.Length - 1 && !char.IsLetter(unit[i + 1])))) { identifierIndex = i; break; diff --git a/Source/DataTypes/SvgViewBox.cs b/Source/DataTypes/SvgViewBox.cs index 7795fd9..e545e4b 100644 --- a/Source/DataTypes/SvgViewBox.cs +++ b/Source/DataTypes/SvgViewBox.cs @@ -126,7 +126,14 @@ namespace Svg public void AddViewBoxTransform(SvgAspectRatio aspectRatio, ISvgRenderer renderer, SvgFragment frag) { - if (this.Equals(SvgViewBox.Empty)) return; + var x = (frag == null ? 0 : frag.X.ToDeviceValue(renderer, UnitRenderingType.Horizontal, frag)); + var y = (frag == null ? 0 : frag.Y.ToDeviceValue(renderer, UnitRenderingType.Vertical, frag)); + + if (this.Equals(SvgViewBox.Empty)) + { + renderer.TranslateTransform(x, y); + return; + } var width = (frag == null ? this.Width : frag.Width.ToDeviceValue(renderer, UnitRenderingType.Horizontal, frag)); var height = (frag == null ? this.Height : frag.Height.ToDeviceValue(renderer, UnitRenderingType.Vertical, frag)); @@ -191,9 +198,6 @@ namespace Svg } } - var x = (frag == null ? 0 : frag.X.ToDeviceValue(renderer, UnitRenderingType.Horizontal, frag)); - var y = (frag == null ? 0 : frag.Y.ToDeviceValue(renderer, UnitRenderingType.Vertical, frag)); - renderer.SetClip(new Region(new RectangleF(x, y, width, height)), CombineMode.Intersect); renderer.ScaleTransform(fScaleX, fScaleY, MatrixOrder.Prepend); renderer.TranslateTransform(x, y); diff --git a/Source/Document Structure/SvgFragment.cs b/Source/Document Structure/SvgFragment.cs index 36f1515..6233669 100644 --- a/Source/Document Structure/SvgFragment.cs +++ b/Source/Document Structure/SvgFragment.cs @@ -157,6 +157,34 @@ namespace Svg this.ViewBox.AddViewBoxTransform(this.AspectRatio, renderer, this); return true; } + + protected override void Render(ISvgRenderer renderer) + { + switch (this.Overflow) + { + case SvgOverflow.auto: + case SvgOverflow.visible: + case SvgOverflow.scroll: + base.Render(renderer); + break; + default: + var prevClip = renderer.GetClip(); + try + { + var size = (this.Parent == null ? renderer.GetBoundable().Bounds.Size : GetDimensions()); + var clip = new RectangleF(this.X.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this), + this.Y.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this), + size.Width, size.Height); + renderer.SetClip(new Region(clip), CombineMode.Intersect); + base.Render(renderer); + } + finally + { + renderer.SetClip(prevClip, CombineMode.Replace); + } + break; + } + } /// /// Gets the for this element. diff --git a/Source/Document Structure/SvgUse.cs b/Source/Document Structure/SvgUse.cs index 5987487..3ed957d 100644 --- a/Source/Document Structure/SvgUse.cs +++ b/Source/Document Structure/SvgUse.cs @@ -74,7 +74,7 @@ namespace Svg { this.SetClip(renderer); - var element = (SvgVisualElement)this.OwnerDocument.IdManager.GetElementById(this.ReferencedElement); + var element = this.OwnerDocument.IdManager.GetElementById(this.ReferencedElement) as SvgVisualElement; if (element != null) { var origParent = element.Parent; diff --git a/Source/Painting/EnumConverters.cs b/Source/Painting/EnumConverters.cs index b0f6636..d048d14 100644 --- a/Source/Painting/EnumConverters.cs +++ b/Source/Painting/EnumConverters.cs @@ -137,6 +137,23 @@ namespace Svg } } + public sealed class SvgCoordinateUnitsConverter : EnumBaseConverter + { + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value == null || value.ToString() == "") return SvgCoordinateUnits.Inherit; + return base.ConvertFrom(context, culture, value); + } + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (destinationType == typeof(string) && value is SvgCoordinateUnits && (SvgCoordinateUnits)value == SvgCoordinateUnits.Inherit) + { + return null; + } + return base.ConvertTo(context, culture, value, destinationType); + } + } + public sealed class SvgTextDecorationConverter : EnumBaseConverter { public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) diff --git a/Source/Painting/SvgColourServer.cs b/Source/Painting/SvgColourServer.cs index 67fa17d..44d07cd 100644 --- a/Source/Painting/SvgColourServer.cs +++ b/Source/Painting/SvgColourServer.cs @@ -35,11 +35,11 @@ namespace Svg set { this._colour = value; } } - public override Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity) + public override Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity, bool forStroke = false) { //is none? if (this == SvgPaintServer.None) return new SolidBrush(System.Drawing.Color.Transparent); - + int alpha = (int)((opacity * (this.Colour.A/255.0f) ) * 255); Color colour = System.Drawing.Color.FromArgb(alpha, this.Colour); diff --git a/Source/Painting/SvgDeferredPaintServer.cs b/Source/Painting/SvgDeferredPaintServer.cs index cb67518..46e93cf 100644 --- a/Source/Painting/SvgDeferredPaintServer.cs +++ b/Source/Painting/SvgDeferredPaintServer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Drawing; namespace Svg { @@ -44,10 +45,10 @@ namespace Svg } } - public override System.Drawing.Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity) + public override Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity, bool forStroke = false) { EnsureServer(styleOwner); - return _concreteServer.GetBrush(styleOwner, renderer, opacity); + return _concreteServer.GetBrush(styleOwner, renderer, opacity, forStroke); } public override SvgElement DeepCopy() diff --git a/Source/Painting/SvgFallbackPaintServer .cs b/Source/Painting/SvgFallbackPaintServer .cs new file mode 100644 index 0000000..c47136c --- /dev/null +++ b/Source/Painting/SvgFallbackPaintServer .cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Drawing; + +namespace Svg +{ + /// + /// A wrapper for a paint server has a fallback if the primary server doesn't work. + /// + public class SvgFallbackPaintServer : SvgPaintServer + { + private IEnumerable _fallbacks; + private SvgPaintServer _primary; + + public SvgFallbackPaintServer() : base() { } + public SvgFallbackPaintServer(SvgPaintServer primary, IEnumerable fallbacks) : this() + { + _fallbacks = fallbacks; + _primary = primary; + } + + public override Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity, bool forStroke = false) + { + try + { + _primary.GetCallback = () => _fallbacks.FirstOrDefault(); + return _primary.GetBrush(styleOwner, renderer, opacity, forStroke); + } + finally + { + _primary.GetCallback = null; + } + } + + public override SvgElement DeepCopy() + { + return base.DeepCopy(); + } + public override SvgElement DeepCopy() + { + var newObj = base.DeepCopy() as SvgFallbackPaintServer; + newObj._fallbacks = this._fallbacks; + newObj._primary = this._primary; + return newObj; + } + } +} diff --git a/Source/Painting/SvgGradientServer.cs b/Source/Painting/SvgGradientServer.cs index aea4892..37a1179 100644 --- a/Source/Painting/SvgGradientServer.cs +++ b/Source/Painting/SvgGradientServer.cs @@ -88,7 +88,7 @@ namespace Svg /// /// Gets or sets another gradient fill from which to inherit the stops from. /// - [SvgAttribute("href")] + [SvgAttribute("href", SvgAttributeAttribute.XLinkNamespace)] public SvgPaintServer InheritGradient { get { return this._inheritGradient; } @@ -101,14 +101,8 @@ namespace Svg [SvgAttribute("gradientTransform")] public SvgTransformCollection GradientTransform { - get - { - return (this.Attributes.GetAttribute("gradientTransform")); - } - set - { - this.Attributes["gradientTransform"] = value; - } + get { return (this.Attributes.GetAttribute("gradientTransform")); } + set { this.Attributes["gradientTransform"] = value; } } protected Matrix EffectiveGradientTransform @@ -186,7 +180,7 @@ namespace Svg var currentStop = this.Stops[radial ? this.Stops.Count - 1 - actualStops : actualStops]; var boundWidth = renderer.GetBoundable().Bounds.Width; - mergedOpacity = opacity * currentStop.Opacity; + mergedOpacity = opacity * currentStop.GetOpacity(); position = radial ? 1 - (currentStop.Offset.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this) / boundWidth) @@ -229,33 +223,6 @@ namespace Svg } } - protected PointF TransformPoint(PointF originalPoint) - { - var newPoint = new[] { originalPoint }; - - EffectiveGradientTransform.TransformPoints(newPoint); - - return newPoint[0]; - } - - protected PointF TransformVector(PointF originalVector) - { - var newVector = new[] { originalVector }; - - EffectiveGradientTransform.TransformVectors(newVector); - - return newVector[0]; - } - - protected float TransformDistance(float dist) - { - var newVector = new[] { new PointF(dist, 0) }; - - EffectiveGradientTransform.TransformVectors(newVector); - - return (float)Math.Sqrt(Math.Pow(newVector[0].X, 2) + Math.Pow(newVector[0].Y, 2)); - } - protected static double CalculateDistance(PointF first, PointF second) { return Math.Sqrt(Math.Pow(first.X - second.X, 2) + Math.Pow(first.Y - second.Y, 2)); diff --git a/Source/Painting/SvgGradientStop.cs b/Source/Painting/SvgGradientStop.cs index 9980b34..aa75ab8 100644 --- a/Source/Painting/SvgGradientStop.cs +++ b/Source/Painting/SvgGradientStop.cs @@ -13,9 +13,7 @@ namespace Svg public class SvgGradientStop : SvgElement { private SvgUnit _offset; - private SvgPaintServer _colour; - private float _opacity; - + /// /// Gets or sets the offset, i.e. where the stop begins from the beginning, of the gradient stop. /// @@ -59,20 +57,31 @@ namespace Svg /// [SvgAttribute("stop-color")] [TypeConverter(typeof(SvgPaintServerFactory))] - public SvgPaintServer Colour + public SvgPaintServer StopColor { - get { return this._colour; } - set { this._colour = value; } + get + { + var direct = this.Attributes.GetAttribute("stop-color", SvgColourServer.NotSet); + if (direct == SvgColourServer.Inherit) return this.Attributes["stop-color"] as SvgPaintServer ?? SvgColourServer.NotSet; + return direct; + } + set { this.Attributes["stop-color"] = value; } } /// /// Gets or sets the opacity of the gradient stop (0-1). /// [SvgAttribute("stop-opacity")] - public float Opacity + public string Opacity { - get { return this._opacity; } - set { this._opacity = value; } + get { return this.Attributes["stop-opacity"] as string; } + set { this.Attributes["stop-opacity"] = value; } + } + + public float GetOpacity() + { + var opacity = this.Opacity; + return string.IsNullOrEmpty(opacity) ? 1.0f : float.Parse(opacity); } /// @@ -81,8 +90,6 @@ namespace Svg public SvgGradientStop() { this._offset = new SvgUnit(0.0f); - this._colour = SvgColourServer.NotSet; - this._opacity = 1.0f; } /// @@ -93,13 +100,11 @@ namespace Svg public SvgGradientStop(SvgUnit offset, Color colour) { this._offset = offset; - this._colour = new SvgColourServer(colour); - this._opacity = 1.0f; } public Color GetColor(SvgElement parent) { - var core = SvgDeferredPaintServer.TryGet(_colour, parent); + var core = SvgDeferredPaintServer.TryGet(this.StopColor, parent); if (core == null) throw new InvalidOperationException("Invalid paint server for gradient stop detected."); return core.Colour; } @@ -113,9 +118,6 @@ namespace Svg { var newObj = base.DeepCopy() as SvgGradientStop; newObj.Offset = this.Offset; - newObj.Colour = this.Colour; - newObj.Opacity = this.Opacity; - return newObj; } } diff --git a/Source/Painting/SvgLinearGradientServer.cs b/Source/Painting/SvgLinearGradientServer.cs index f419e76..e373aae 100644 --- a/Source/Painting/SvgLinearGradientServer.cs +++ b/Source/Painting/SvgLinearGradientServer.cs @@ -79,36 +79,94 @@ namespace Svg Y2 = new SvgUnit(SvgUnitType.Percentage, 0F); } - public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity) + public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity, bool forStroke = false) { LoadStops(renderingElement); - if (IsInvalid) + + if (this.Stops.Count < 1) return null; + if (this.Stops.Count == 1) { - return null; + var stopColor = this.Stops[0].GetColor(renderingElement); + int alpha = (int)((opacity * (stopColor.A/255.0f) ) * 255); + Color colour = System.Drawing.Color.FromArgb(alpha, stopColor); + return new SolidBrush(colour); } try { if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.SetBoundable(renderingElement); - var specifiedStart = CalculateStart(renderer); - var specifiedEnd = CalculateEnd(renderer); + var points = new PointF[] { + SvgUnit.GetDevicePoint(NormalizeUnit(this.X1), NormalizeUnit(this.Y1), renderer, this), + SvgUnit.GetDevicePoint(NormalizeUnit(this.X2), NormalizeUnit(this.Y2), renderer, this) + }; + + var bounds = renderer.GetBoundable().Bounds; + if (bounds.Width <= 0 || bounds.Height <= 0) + { + if (this.GetCallback != null) return GetCallback().GetBrush(renderingElement, renderer, opacity, forStroke); + return null; + } + + using (var transform = EffectiveGradientTransform) + { + var midPoint = new PointF((points[0].X + points[1].X) / 2, (points[0].Y + points[1].Y) / 2); + transform.Translate(bounds.X, bounds.Y, MatrixOrder.Prepend); + if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) + { + // Transform a normal (i.e. perpendicular line) according to the transform + transform.Scale(bounds.Width, bounds.Height, MatrixOrder.Prepend); + transform.RotateAt(-90.0f, midPoint, MatrixOrder.Prepend); + } + transform.TransformPoints(points); + } + + if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) + { + // Transform the normal line back to a line such that the gradient still starts in the correct corners, but + // has the proper normal vector based on the transforms. If you work out the geometry, these formulas should work. + var midPoint = new PointF((points[0].X + points[1].X) / 2, (points[0].Y + points[1].Y) / 2); + var dy = (points[1].Y - points[0].Y); + var dx = (points[1].X - points[0].X); + var x2 = points[0].X; + var y2 = points[1].Y; + + if (Math.Round(dx, 4) == 0) + { + points[0] = new PointF(midPoint.X + dy / 2 * bounds.Width / bounds.Height, midPoint.Y); + points[1] = new PointF(midPoint.X - dy / 2 * bounds.Width / bounds.Height, midPoint.Y); + } + else if (Math.Round(dy, 4) == 0) + { + points[0] = new PointF(midPoint.X, midPoint.Y - dx / 2 * bounds.Height / bounds.Width); + points[1] = new PointF(midPoint.X, midPoint.Y + dx / 2 * bounds.Height / bounds.Width); ; + } + else + { + var startX = (float)((dy * dx * (midPoint.Y - y2) + Math.Pow(dx, 2) * midPoint.X + Math.Pow(dy, 2) * x2) / + (Math.Pow(dx, 2) + Math.Pow(dy, 2))); + var startY = dy * (startX - x2) / dx + y2; + points[0] = new PointF(startX, startY); + points[1] = new PointF(midPoint.X + (midPoint.X - startX), midPoint.Y + (midPoint.Y - startY)); + } + } - var effectiveStart = specifiedStart; - var effectiveEnd = specifiedEnd; + var effectiveStart = points[0]; + var effectiveEnd = points[1]; - if (NeedToExpandGradient(renderingElement, specifiedStart, specifiedEnd)) + if (PointsToMove(renderingElement, points[0], points[1]) > LinePoints.None) { - var expansion = ExpandGradient(renderingElement, specifiedStart, specifiedEnd); + var expansion = ExpandGradient(renderingElement, points[0], points[1]); effectiveStart = expansion.StartPoint; effectiveEnd = expansion.EndPoint; } - return new LinearGradientBrush(effectiveStart, effectiveEnd, System.Drawing.Color.Transparent, System.Drawing.Color.Transparent) + var result = new LinearGradientBrush(effectiveStart, effectiveEnd, System.Drawing.Color.Transparent, System.Drawing.Color.Transparent) { - InterpolationColors = CalculateColorBlend(renderer, opacity, specifiedStart, effectiveStart, specifiedEnd, effectiveEnd), + InterpolationColors = CalculateColorBlend(renderer, opacity, points[0], effectiveStart, points[1], effectiveEnd), WrapMode = WrapMode.TileFlipX }; + return result; } finally { @@ -116,19 +174,36 @@ namespace Svg } } - private PointF CalculateStart(ISvgRenderer renderer) + private SvgUnit NormalizeUnit(SvgUnit orig) { - return TransformPoint(SvgUnit.GetDevicePointOffset(this.X1, this.Y1, renderer, this)); + return (orig.Type == SvgUnitType.Percentage && this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox ? + new SvgUnit(SvgUnitType.User, orig.Value / 100) : + orig); } - private PointF CalculateEnd(ISvgRenderer renderer) + [Flags] + private enum LinePoints { - return TransformPoint(SvgUnit.GetDevicePointOffset(this.X2, this.Y2, renderer, this)); + None = 0, + Start = 1, + End = 2 } - private bool NeedToExpandGradient(ISvgBoundable boundable, PointF specifiedStart, PointF specifiedEnd) + private LinePoints PointsToMove(ISvgBoundable boundable, PointF specifiedStart, PointF specifiedEnd) { - return SpreadMethod == SvgGradientSpreadMethod.Pad && (boundable.Bounds.Contains(specifiedStart) || boundable.Bounds.Contains(specifiedEnd)); + var bounds = boundable.Bounds; + if (specifiedStart.X == specifiedEnd.X) + { + return (bounds.Top < specifiedStart.Y && specifiedStart.Y < bounds.Bottom ? LinePoints.Start : LinePoints.None) | + (bounds.Top < specifiedEnd.Y && specifiedEnd.Y < bounds.Bottom ? LinePoints.End : LinePoints.None); + } + else if (specifiedStart.Y == specifiedEnd.Y) + { + return (bounds.Left < specifiedStart.X && specifiedStart.X < bounds.Right ? LinePoints.Start : LinePoints.None) | + (bounds.Left < specifiedEnd.X && specifiedEnd.X < bounds.Right ? LinePoints.End : LinePoints.None); + } + return (boundable.Bounds.Contains(specifiedStart) ? LinePoints.Start : LinePoints.None) | + (boundable.Bounds.Contains(specifiedEnd) ? LinePoints.End : LinePoints.None); } public struct GradientPoints @@ -145,44 +220,98 @@ namespace Svg private GradientPoints ExpandGradient(ISvgBoundable boundable, PointF specifiedStart, PointF specifiedEnd) { - if (!NeedToExpandGradient(boundable, specifiedStart, specifiedEnd)) + var pointsToMove = PointsToMove(boundable, specifiedStart, specifiedEnd); + if (pointsToMove == LinePoints.None) { Debug.Fail("Unexpectedly expanding gradient when not needed!"); return new GradientPoints(specifiedStart, specifiedEnd); } - var specifiedLength = CalculateDistance(specifiedStart, specifiedEnd); - var specifiedUnitVector = new PointF((specifiedEnd.X - specifiedStart.X) / (float)specifiedLength, (specifiedEnd.Y - specifiedStart.Y) / (float)specifiedLength); - + var bounds = boundable.Bounds; var effectiveStart = specifiedStart; var effectiveEnd = specifiedEnd; + var intersectionPoints = CandidateIntersections(bounds, specifiedStart, specifiedEnd); - var elementDiagonal = (float)CalculateDistance(new PointF(boundable.Bounds.Left, boundable.Bounds.Top), new PointF(boundable.Bounds.Right, boundable.Bounds.Bottom)); + Debug.Assert(intersectionPoints.Count == 2, "Unanticipated number of intersection points"); - var expandedStart = MovePointAlongVector(effectiveStart, specifiedUnitVector, -elementDiagonal); - var expandedEnd = MovePointAlongVector(effectiveEnd, specifiedUnitVector, elementDiagonal); + if (!(Math.Sign(intersectionPoints[1].X - intersectionPoints[0].X) == Math.Sign(specifiedEnd.X - specifiedStart.X) && + Math.Sign(intersectionPoints[1].Y - intersectionPoints[0].Y) == Math.Sign(specifiedEnd.Y - specifiedStart.Y))) + { + intersectionPoints = intersectionPoints.Reverse().ToList(); + } - var intersectionPoints = new LineF(expandedStart.X, expandedStart.Y, expandedEnd.X, expandedEnd.Y).Intersection(boundable.Bounds); + if ((pointsToMove & LinePoints.Start) > 0) effectiveStart = intersectionPoints[0]; + if ((pointsToMove & LinePoints.End) > 0) effectiveEnd = intersectionPoints[1]; - if (boundable.Bounds.Contains(specifiedStart)) + switch (SpreadMethod) { - effectiveStart = CalculateClosestIntersectionPoint(expandedStart, intersectionPoints); - - effectiveStart = MovePointAlongVector(effectiveStart, specifiedUnitVector, -1); + case SvgGradientSpreadMethod.Reflect: + case SvgGradientSpreadMethod.Repeat: + var specifiedLength = CalculateDistance(specifiedStart, specifiedEnd); + var specifiedUnitVector = new PointF((specifiedEnd.X - specifiedStart.X) / (float)specifiedLength, (specifiedEnd.Y - specifiedStart.Y) / (float)specifiedLength); + var oppUnitVector = new PointF(-specifiedUnitVector.X, -specifiedUnitVector.Y); + + var startExtend = (float)(Math.Ceiling(CalculateDistance(effectiveStart, specifiedStart) / specifiedLength) * specifiedLength); + effectiveStart = MovePointAlongVector(specifiedStart, oppUnitVector, startExtend); + var endExtend = (float)(Math.Ceiling(CalculateDistance(effectiveEnd, specifiedEnd) / specifiedLength) * specifiedLength); + effectiveEnd = MovePointAlongVector(specifiedEnd, specifiedUnitVector, endExtend); + break; } - if (boundable.Bounds.Contains(specifiedEnd)) - { - effectiveEnd = CalculateClosestIntersectionPoint(expandedEnd, intersectionPoints); + return new GradientPoints(effectiveStart, effectiveEnd); + } - effectiveEnd = MovePointAlongVector(effectiveEnd, specifiedUnitVector, 1); + private IList CandidateIntersections(RectangleF bounds, PointF p1, PointF p2) + { + var results = new List(); + if (Math.Round(Math.Abs(p1.Y - p2.Y), 4) == 0) + { + results.Add(new PointF(bounds.Left, p1.Y)); + results.Add(new PointF(bounds.Right, p1.Y)); + } + else if (Math.Round(Math.Abs(p1.X - p2.X), 4) == 0) + { + results.Add(new PointF(p1.X, bounds.Top)); + results.Add(new PointF(p1.X, bounds.Bottom)); + } + else + { + PointF candidate; + // Save some effort and duplication in the trivial case + if ((p1.X == bounds.Left || p1.X == bounds.Right) && (p1.Y == bounds.Top || p1.Y == bounds.Bottom)) + { + results.Add(p1); + } + else + { + candidate = new PointF(bounds.Left, (p2.Y - p1.Y) / (p2.X - p1.X) * (bounds.Left - p1.X) + p1.Y); + if (bounds.Top <= candidate.Y && candidate.Y <= bounds.Bottom) results.Add(candidate); + candidate = new PointF(bounds.Right, (p2.Y - p1.Y) / (p2.X - p1.X) * (bounds.Right - p1.X) + p1.Y); + if (bounds.Top <= candidate.Y && candidate.Y <= bounds.Bottom) results.Add(candidate); + } + if ((p2.X == bounds.Left || p2.X == bounds.Right) && (p2.Y == bounds.Top || p2.Y == bounds.Bottom)) + { + results.Add(p2); + } + else + { + candidate = new PointF((bounds.Top - p1.Y) / (p2.Y - p1.Y) * (p2.X - p1.X) + p1.X, bounds.Top); + if (bounds.Left <= candidate.X && candidate.X <= bounds.Right) results.Add(candidate); + candidate = new PointF((bounds.Bottom - p1.Y) / (p2.Y - p1.Y) * (p2.X - p1.X) + p1.X, bounds.Bottom); + if (bounds.Left <= candidate.X && candidate.X <= bounds.Right) results.Add(candidate); + } } - return new GradientPoints(effectiveStart, effectiveEnd); + return results; } private ColorBlend CalculateColorBlend(ISvgRenderer renderer, float opacity, PointF specifiedStart, PointF effectiveStart, PointF specifiedEnd, PointF effectiveEnd) { + float startExtend; + float endExtend; + List colors; + List positions; + var colorBlend = GetColorBlend(renderer, opacity, false); var startDelta = CalculateDistance(specifiedStart, effectiveStart); @@ -198,25 +327,101 @@ namespace Svg var effectiveLength = CalculateDistance(effectiveStart, effectiveEnd); - for (var i = 0; i < colorBlend.Positions.Length; i++) + switch (SpreadMethod) { - var originalPoint = MovePointAlongVector(specifiedStart, specifiedUnitVector, (float)specifiedLength * colorBlend.Positions[i]); + case SvgGradientSpreadMethod.Reflect: + startExtend = (float)(Math.Ceiling(CalculateDistance(effectiveStart, specifiedStart) / specifiedLength)); + endExtend = (float)(Math.Ceiling(CalculateDistance(effectiveEnd, specifiedEnd) / specifiedLength)); + colors = colorBlend.Colors.ToList(); + positions = (from p in colorBlend.Positions select p + startExtend).ToList(); - var distanceFromEffectiveStart = CalculateDistance(effectiveStart, originalPoint); + for (var i = 0; i < startExtend; i++) + { + if (i % 2 == 0) + { + for (var j = 1; j < colorBlend.Positions.Length; j++) + { + positions.Insert(0, (float)((startExtend - 1 - i) + 1 - colorBlend.Positions[j])); + colors.Insert(0, colorBlend.Colors[j]); + } + } + else + { + for (var j = 0; j < colorBlend.Positions.Length - 1; j++) + { + positions.Insert(j, (float)((startExtend - 1 - i) + colorBlend.Positions[j])); + colors.Insert(j, colorBlend.Colors[j]); + } + } + } - colorBlend.Positions[i] = (float)Math.Round(Math.Max(0F, Math.Min((distanceFromEffectiveStart / effectiveLength), 1.0F)), 5); - } + int insertPos; + for (var i = 0; i < endExtend; i++) + { + if (i % 2 == 0) + { + insertPos = positions.Count; + for (var j = 0; j < colorBlend.Positions.Length - 1; j++) + { + positions.Insert(insertPos, (float)((startExtend + 1 + i) + 1 - colorBlend.Positions[j])); + colors.Insert(insertPos, colorBlend.Colors[j]); + } + } + else + { + for (var j = 1; j < colorBlend.Positions.Length; j++) + { + positions.Add((float)((startExtend + 1 + i) + colorBlend.Positions[j])); + colors.Add(colorBlend.Colors[j]); + } + } + } - if (startDelta > 0) - { - colorBlend.Positions = new[] { 0F }.Concat(colorBlend.Positions).ToArray(); - colorBlend.Colors = new[] { colorBlend.Colors.First() }.Concat(colorBlend.Colors).ToArray(); - } + colorBlend.Colors = colors.ToArray(); + colorBlend.Positions = (from p in positions select p / (startExtend + 1 + endExtend)).ToArray(); + break; + case SvgGradientSpreadMethod.Repeat: + startExtend = (float)(Math.Ceiling(CalculateDistance(effectiveStart, specifiedStart) / specifiedLength)); + endExtend = (float)(Math.Ceiling(CalculateDistance(effectiveEnd, specifiedEnd) / specifiedLength)); + colors = new List(); + positions = new List(); - if (endDelta > 0) - { - colorBlend.Positions = colorBlend.Positions.Concat(new[] { 1F }).ToArray(); - colorBlend.Colors = colorBlend.Colors.Concat(new[] { colorBlend.Colors.Last() }).ToArray(); + for (int i = 0; i < startExtend + endExtend + 1; i++) + { + for (int j = 0; j < colorBlend.Positions.Length; j++) + { + positions.Add((i + colorBlend.Positions[j] * 0.9999f) / (startExtend + endExtend + 1)); + colors.Add(colorBlend.Colors[j]); + } + } + positions[positions.Count - 1] = 1.0f; + + colorBlend.Colors = colors.ToArray(); + colorBlend.Positions = positions.ToArray(); + + break; + default: + for (var i = 0; i < colorBlend.Positions.Length; i++) + { + var originalPoint = MovePointAlongVector(specifiedStart, specifiedUnitVector, (float)specifiedLength * colorBlend.Positions[i]); + + var distanceFromEffectiveStart = CalculateDistance(effectiveStart, originalPoint); + + colorBlend.Positions[i] = (float)Math.Round(Math.Max(0F, Math.Min((distanceFromEffectiveStart / effectiveLength), 1.0F)), 5); + } + + if (startDelta > 0) + { + colorBlend.Positions = new[] { 0F }.Concat(colorBlend.Positions).ToArray(); + colorBlend.Colors = new[] { colorBlend.Colors.First() }.Concat(colorBlend.Colors).ToArray(); + } + + if (endDelta > 0) + { + colorBlend.Positions = colorBlend.Positions.Concat(new[] { 1F }).ToArray(); + colorBlend.Colors = colorBlend.Colors.Concat(new[] { colorBlend.Colors.Last() }).ToArray(); + } + break; } return colorBlend; @@ -335,48 +540,6 @@ namespace Svg return null; } } - - - //var a1 = Y2 - Y1; - //var b1 = X1 - X2; - //var c1 = X2 * Y1 - X1 * Y2; - - //var r3 = a1 * other.X1 + b1 * other.Y1 + c1; - //var r4 = a1 * other.X2 + b1 * other.Y2 + c1; - - //if (r3 != 0 && r4 != 0 && Math.Sign(r3) == Math.Sign(r4)) - //{ - // return null; - //} - - //var a2 = other.Y2 - other.Y1; - //var b2 = other.X1 - other.X2; - //var c2 = other.X2 * other.Y1 - other.X1 * other.Y2; - - //var r1 = a2 * X1 + b2 * Y1 + c2; - //var r2 = a2 * X2 + b2 * Y2 + c2; - - //if (r1 != 0 && r2 != 0 && Math.Sign(r1) == Math.Sign(r2)) - //{ - // return (null); - //} - - //var denom = a1 * b2 - a2 * b1; - - //if (denom == 0) - //{ - // return null; - //} - - //var offset = denom < 0 ? -denom / 2 : denom / 2; - - //var num = b1 * c2 - b2 * c1; - //var x = (num < 0 ? num - offset : num + offset) / denom; - - //num = a2 * c1 - a1 * c2; - //var y = (num < 0 ? num - offset : num + offset) / denom; - - //return new PointF(x, y); } private static void AddIfIntersect(LineF first, LineF second, ICollection result) diff --git a/Source/Painting/SvgPaintServer.cs b/Source/Painting/SvgPaintServer.cs index 417024c..0d53403 100644 --- a/Source/Painting/SvgPaintServer.cs +++ b/Source/Painting/SvgPaintServer.cs @@ -13,6 +13,8 @@ namespace Svg [TypeConverter(typeof(SvgPaintServerFactory))] public abstract class SvgPaintServer : SvgElement { + public Func GetCallback { get; set; } + /// /// An unspecified . /// @@ -39,7 +41,7 @@ namespace Svg /// /// The owner . /// The opacity of the brush. - public abstract Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity); + public abstract Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity, bool forStroke = false); /// /// Returns a that represents the current . diff --git a/Source/Painting/SvgPaintServerFactory.cs b/Source/Painting/SvgPaintServerFactory.cs index 54a4ea5..0514e60 100644 --- a/Source/Painting/SvgPaintServerFactory.cs +++ b/Source/Painting/SvgPaintServerFactory.cs @@ -1,23 +1,19 @@ using System; -using System.ComponentModel; using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml; +using System.ComponentModel; using System.Drawing; using System.Globalization; +using System.Linq; namespace Svg { internal class SvgPaintServerFactory : TypeConverter { private static readonly SvgColourConverter _colourConverter; - private static readonly Regex _urlRefPattern; static SvgPaintServerFactory() { _colourConverter = new SvgColourConverter(); - _urlRefPattern = new Regex(@"url\((#[^)]+)\)"); } public static SvgPaintServer Create(string value, SvgDocument document) @@ -35,34 +31,71 @@ namespace Svg { return new SvgDeferredPaintServer(document, value); } - else if (value.IndexOf("url(#") > -1) - { - Match match = _urlRefPattern.Match(value); - Uri id = new Uri(match.Groups[1].Value, UriKind.Relative); - return (SvgPaintServer)document.IdManager.GetElementById(id); - } - // If referenced to to a different (linear or radial) gradient - else if (document.IdManager.GetElementById(value) != null && document.IdManager.GetElementById(value).GetType().BaseType == typeof(SvgGradientServer)) - { - return (SvgPaintServer)document.IdManager.GetElementById(value); - } - else if (value.StartsWith("#")) // Otherwise try and parse as colour + else { - try + var servers = new List(); + + while (!string.IsNullOrEmpty(value)) { - return new SvgColourServer((Color)_colourConverter.ConvertFrom(value.Trim())); + if (value.StartsWith("url(#")) + { + var leftParen = value.IndexOf(')', 5); + Uri id = new Uri(value.Substring(5, leftParen - 5), UriKind.Relative); + value = value.Substring(leftParen + 1).Trim(); + servers.Add((SvgPaintServer)document.IdManager.GetElementById(id)); + } + // If referenced to to a different (linear or radial) gradient + else if (document.IdManager.GetElementById(value) != null && document.IdManager.GetElementById(value).GetType().BaseType == typeof(SvgGradientServer)) + { + return (SvgPaintServer)document.IdManager.GetElementById(value); + } + else if (value.StartsWith("#")) // Otherwise try and parse as colour + { + switch(CountHexDigits(value, 1)) + { + case 3: + servers.Add(new SvgColourServer((Color)_colourConverter.ConvertFrom(value.Substring(0, 4)))); + value = value.Substring(4).Trim(); + break; + case 6: + servers.Add(new SvgColourServer((Color)_colourConverter.ConvertFrom(value.Substring(0, 7)))); + value = value.Substring(7).Trim(); + break; + default: + return new SvgDeferredPaintServer(document, value); + } + } + else + { + return new SvgColourServer((Color)_colourConverter.ConvertFrom(value.Trim())); + } } - catch + + if (servers.Count > 1) { - return new SvgDeferredPaintServer(document, value); + return new SvgFallbackPaintServer(servers[0], servers.Skip(1)); } - } - else + return servers[0]; + } + + + } + + private static int CountHexDigits(string value, int start) + { + int i = Math.Max(start, 0); + int count = 0; + while (i < value.Length && + ((value[i] >= '0' && value[i] <= '9') || + (value[i] >= 'a' && value[i] <= 'f') || + (value[i] >= 'A' && value[i] <= 'F'))) { - return new SvgColourServer((Color)_colourConverter.ConvertFrom(value.Trim())); + count++; + i++; } + return count; } - + public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { if (value is string) diff --git a/Source/Painting/SvgPatternServer.cs b/Source/Painting/SvgPatternServer.cs index 48adb39..dba7af9 100644 --- a/Source/Painting/SvgPatternServer.cs +++ b/Source/Painting/SvgPatternServer.cs @@ -6,6 +6,7 @@ using System.Drawing; using System.ComponentModel; using Svg.Transforms; +using System.Linq; namespace Svg { @@ -19,9 +20,10 @@ namespace Svg private SvgUnit _height; private SvgUnit _x; private SvgUnit _y; + private SvgPaintServer _inheritGradient; private SvgViewBox _viewBox; - private SvgCoordinateUnits _patternUnits = SvgCoordinateUnits.ObjectBoundingBox; - private SvgCoordinateUnits _patternContentUnits = SvgCoordinateUnits.UserSpaceOnUse; + private SvgCoordinateUnits _patternUnits = SvgCoordinateUnits.Inherit; + private SvgCoordinateUnits _patternContentUnits = SvgCoordinateUnits.Inherit; [SvgAttribute("overflow")] public SvgOverflow Overflow @@ -76,7 +78,7 @@ namespace Svg /// /// Gets or sets the width of the pattern. /// - [SvgAttribute("patternUnits")] + [SvgAttribute("patternContentUnits")] public SvgCoordinateUnits PatternContentUnits { get { return this._patternContentUnits; } @@ -113,15 +115,56 @@ namespace Svg set { this._y = value; } } + /// + /// Gets or sets another gradient fill from which to inherit the stops from. + /// + [SvgAttribute("href", SvgAttributeAttribute.XLinkNamespace)] + public SvgPaintServer InheritGradient + { + get { return this._inheritGradient; } + set + { + this._inheritGradient = value; + } + } + + [SvgAttribute("patternTransform")] + public SvgTransformCollection PatternTransform + { + get { return (this.Attributes.GetAttribute("gradientTransform")); } + set { this.Attributes["gradientTransform"] = value; } + } + + protected Matrix EffectivePatternTransform + { + get + { + var transform = new Matrix(); + + if (PatternTransform != null) + { + transform.Multiply(PatternTransform.GetMatrix()); + } + return transform; + } + } + /// /// Initializes a new instance of the class. /// public SvgPatternServer() { - this._x = new SvgUnit(0.0f); - this._y = new SvgUnit(0.0f); - this._width = new SvgUnit(0.0f); - this._height = new SvgUnit(0.0f); + this._x = SvgUnit.None; + this._y = SvgUnit.None; + this._width = SvgUnit.None; + this._height = SvgUnit.None; + } + + private SvgUnit NormalizeUnit(SvgUnit orig) + { + return (orig.Type == SvgUnitType.Percentage && this.PatternUnits == SvgCoordinateUnits.ObjectBoundingBox ? + new SvgUnit(SvgUnitType.User, orig.Value / 100) : + orig); } /// @@ -129,57 +172,78 @@ namespace Svg /// /// The owner . /// The opacity of the brush. - public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity) + public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity, bool forStroke = false) { - // If there aren't any children, return null - if (this.Children.Count == 0) - return null; + var chain = new List(); + var curr = this; + while (curr != null) + { + chain.Add(curr); + curr = SvgDeferredPaintServer.TryGet(curr._inheritGradient, renderingElement); + } + + var childElem = chain.Where((p) => p.Children != null && p.Children.Count > 0).FirstOrDefault(); + if (childElem == null) return null; + var widthElem = chain.Where((p) => p.Width != null && p.Width != SvgUnit.None).FirstOrDefault(); + var heightElem = chain.Where((p) => p.Height != null && p.Height != SvgUnit.None).FirstOrDefault(); + if (widthElem == null && heightElem == null) return null; - // Can't render if there are no dimensions - if (this._width.Value == 0.0f || this._height.Value == 0.0f) - return null; + var viewBoxElem = chain.Where((p) => p.ViewBox != null && p.ViewBox != SvgViewBox.Empty).FirstOrDefault(); + var viewBox = viewBoxElem == null ? SvgViewBox.Empty : viewBoxElem.ViewBox; + var xElem = chain.Where((p) => p.X != null && p.X != SvgUnit.None).FirstOrDefault(); + var yElem = chain.Where((p) => p.Y != null && p.Y != SvgUnit.None).FirstOrDefault(); + var xUnit = xElem == null ? SvgUnit.Empty : xElem.X; + var yUnit = yElem == null ? SvgUnit.Empty : yElem.Y; + + var patternUnitElem = chain.Where((p) => p.PatternUnits != SvgCoordinateUnits.Inherit).FirstOrDefault(); + var patternUnits = (patternUnitElem == null ? SvgCoordinateUnits.ObjectBoundingBox : patternUnitElem.PatternUnits); + var patternContentUnitElem = chain.Where((p) => p.PatternContentUnits != SvgCoordinateUnits.Inherit).FirstOrDefault(); + var patternContentUnits = (patternContentUnitElem == null ? SvgCoordinateUnits.UserSpaceOnUse : patternContentUnitElem.PatternContentUnits); try { - if (this.PatternUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.SetBoundable(renderingElement); - - float width = this._width.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this); - float height = this._height.ToDeviceValue(renderer, UnitRenderingType.Vertical, this); + if (patternUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.SetBoundable(renderingElement); - Matrix patternMatrix = new Matrix(); - // Apply a translate if needed - if (this._x.Value > 0.0f || this._y.Value > 0.0f) + using (var patternMatrix = new Matrix()) { - float x = this._x.ToDeviceValue(renderer, UnitRenderingType.HorizontalOffset, this); - float y = this._y.ToDeviceValue(renderer, UnitRenderingType.VerticalOffset, this); + var bounds = renderer.GetBoundable().Bounds; + var xScale = (patternUnits == SvgCoordinateUnits.ObjectBoundingBox ? bounds.Width : 1); + var yScale = (patternUnits == SvgCoordinateUnits.ObjectBoundingBox ? bounds.Height : 1); - patternMatrix.Translate(x, y); - } + float x = xScale * NormalizeUnit(xUnit).ToDeviceValue(renderer, UnitRenderingType.Horizontal, this); + float y = yScale * NormalizeUnit(yUnit).ToDeviceValue(renderer, UnitRenderingType.Vertical, this); - if (this.ViewBox.Height > 0 || this.ViewBox.Width > 0) - { - patternMatrix.Scale(this.Width.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this) / this.ViewBox.Width, - this.Height.ToDeviceValue(renderer, UnitRenderingType.Vertical, this) / this.ViewBox.Height); - } + float width = xScale * NormalizeUnit(widthElem.Width).ToDeviceValue(renderer, UnitRenderingType.Horizontal, this); + float height = yScale * NormalizeUnit(heightElem.Height).ToDeviceValue(renderer, UnitRenderingType.Vertical, this); - Bitmap image = new Bitmap((int)width, (int)height); - using (var iRenderer = SvgRenderer.FromImage(image)) - { - iRenderer.SetBoundable((_patternContentUnits == SvgCoordinateUnits.ObjectBoundingBox) ? new GenericBoundable(0, 0, width, height) : renderer.GetBoundable()); - iRenderer.Transform = patternMatrix; - iRenderer.SmoothingMode = SmoothingMode.AntiAlias; + // Apply a scale if needed + patternMatrix.Scale((patternContentUnits == SvgCoordinateUnits.ObjectBoundingBox ? bounds.Width : 1) * + (viewBox.Width > 0 ? width / viewBox.Width : 1), + (patternContentUnits == SvgCoordinateUnits.ObjectBoundingBox ? bounds.Height : 1) * + (viewBox.Height > 0 ? height / viewBox.Height : 1), MatrixOrder.Prepend); - foreach (SvgElement child in this.Children) + Bitmap image = new Bitmap((int)width, (int)height); + using (var iRenderer = SvgRenderer.FromImage(image)) { - child.RenderElement(iRenderer); - } - } + iRenderer.SetBoundable((_patternContentUnits == SvgCoordinateUnits.ObjectBoundingBox) ? new GenericBoundable(0, 0, width, height) : renderer.GetBoundable()); + iRenderer.Transform = patternMatrix; + iRenderer.SmoothingMode = SmoothingMode.AntiAlias; + iRenderer.SetClip(new Region(new RectangleF(0, 0, + viewBox.Width > 0 ? viewBox.Width : width, + viewBox.Height > 0 ? viewBox.Height : height))); - image.Save(string.Format(@"C:\test{0:D3}.png", imgNumber++)); - - TextureBrush textureBrush = new TextureBrush(image); + foreach (SvgElement child in childElem.Children) + { + child.RenderElement(iRenderer); + } + } - return textureBrush; + TextureBrush textureBrush = new TextureBrush(image); + var brushTransform = EffectivePatternTransform.Clone(); + brushTransform.Translate(x, y, MatrixOrder.Append); + textureBrush.Transform = brushTransform; + return textureBrush; + } } finally { @@ -187,10 +251,6 @@ namespace Svg } } - private static int imgNumber = 0; - - - public override SvgElement DeepCopy() { return DeepCopy(); diff --git a/Source/Painting/SvgRadialGradientServer.cs b/Source/Painting/SvgRadialGradientServer.cs index f6a98bc..981a51f 100644 --- a/Source/Painting/SvgRadialGradientServer.cs +++ b/Source/Painting/SvgRadialGradientServer.cs @@ -98,29 +98,85 @@ namespace Svg private object _lockObj = new Object(); - public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity) + private SvgUnit NormalizeUnit(SvgUnit orig) + { + return (orig.Type == SvgUnitType.Percentage && this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox ? + new SvgUnit(SvgUnitType.User, orig.Value / 100) : + orig); + } + + public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity, bool forStroke = false) { LoadStops(renderingElement); try { if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.SetBoundable(renderingElement); - + // Calculate the path and transform it appropriately - var origin = renderer.GetBoundable().Location; - var center = new PointF(origin.X + CenterX.ToDeviceValue(renderer, UnitRenderingType.HorizontalOffset, this), - origin.Y + CenterY.ToDeviceValue(renderer, UnitRenderingType.VerticalOffset, this)); - var specifiedRadius = Radius.ToDeviceValue(renderer, UnitRenderingType.Other, this); + var center = new PointF(NormalizeUnit(CenterX).ToDeviceValue(renderer, UnitRenderingType.Horizontal, this), + NormalizeUnit(CenterY).ToDeviceValue(renderer, UnitRenderingType.Vertical, this)); + var focals = new PointF[] {new PointF(NormalizeUnit(FocalX).ToDeviceValue(renderer, UnitRenderingType.Horizontal, this), + NormalizeUnit(FocalY).ToDeviceValue(renderer, UnitRenderingType.Vertical, this)) }; + var specifiedRadius = NormalizeUnit(Radius).ToDeviceValue(renderer, UnitRenderingType.Other, this); var path = new GraphicsPath(); path.AddEllipse( - origin.X + center.X - specifiedRadius, origin.Y + center.Y - specifiedRadius, + center.X - specifiedRadius, center.Y - specifiedRadius, specifiedRadius * 2, specifiedRadius * 2 ); - path.Transform(EffectiveGradientTransform); + + using (var transform = EffectiveGradientTransform) + { + var bounds = renderer.GetBoundable().Bounds; + transform.Translate(bounds.X, bounds.Y, MatrixOrder.Prepend); + if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) + { + transform.Scale(bounds.Width, bounds.Height, MatrixOrder.Prepend); + } + path.Transform(transform); + transform.TransformPoints(focals); + } // Calculate any required scaling - var scale = CalcScale(renderingElement.Bounds, path); + var scaleBounds = RectangleF.Inflate(renderingElement.Bounds, renderingElement.StrokeWidth, renderingElement.StrokeWidth); + var scale = CalcScale(scaleBounds, path); + + // Not ideal, but this makes sure that the rest of the shape gets properly filled or drawn + if (scale > 1.0f && SpreadMethod == SvgGradientSpreadMethod.Pad) + { + var stop = Stops.Last(); + var origColor = stop.GetColor(renderingElement); + var renderColor = System.Drawing.Color.FromArgb((int)(opacity * stop.GetOpacity() * 255), origColor); + + var origClip = renderer.GetClip(); + try + { + using (var solidBrush = new SolidBrush(renderColor)) + { + var newClip = origClip.Clone(); + newClip.Exclude(path); + renderer.SetClip(newClip); + + var renderPath = (GraphicsPath)renderingElement.Path(renderer); + if (forStroke) + { + using (var pen = new Pen(solidBrush, renderingElement.StrokeWidth.ToDeviceValue(renderer, UnitRenderingType.Other, renderingElement))) + { + renderer.DrawPath(pen, renderPath); + } + } + else + { + renderer.FillPath(solidBrush, renderPath); + } + } + } + finally + { + renderer.SetClip(origClip); + } + } // Get the color blend and any tweak to the scaling var blend = CalculateColorBlend(renderer, opacity, scale, out scale); @@ -138,7 +194,7 @@ namespace Svg // calculate the brush var brush = new PathGradientBrush(path); - brush.CenterPoint = CalculateFocalPoint(renderer, origin); + brush.CenterPoint = focals[0]; brush.InterpolationColors = blend; return brush; @@ -185,12 +241,62 @@ namespace Svg return bounds.Height / (points[2].Y - points[1].Y); } - private PointF CalculateFocalPoint(ISvgRenderer renderer, PointF origin) + //New plan: + // scale the outer rectangle to always encompass ellipse + // cut the ellipse in half (either vertical or horizontal) + // determine the region on each side of the ellipse + private static IEnumerable GetDifference(RectangleF subject, GraphicsPath clip) { - var deviceFocalX = origin.X + FocalX.ToDeviceValue(renderer, UnitRenderingType.HorizontalOffset, this); - var deviceFocalY = origin.Y + FocalY.ToDeviceValue(renderer, UnitRenderingType.VerticalOffset, this); - var transformedFocalPoint = TransformPoint(new PointF(deviceFocalX, deviceFocalY)); - return transformedFocalPoint; + var clipFlat = (GraphicsPath)clip.Clone(); + clipFlat.Flatten(); + var clipBounds = clipFlat.GetBounds(); + var bounds = RectangleF.Union(subject, clipBounds); + bounds.Inflate(bounds.Width * .3f, bounds.Height * 0.3f); + + var clipMidPoint = new PointF((clipBounds.Left + clipBounds.Right) / 2, (clipBounds.Top + clipBounds.Bottom) / 2); + var leftPoints = new List(); + var rightPoints = new List(); + foreach (var pt in clipFlat.PathPoints) + { + if (pt.X <= clipMidPoint.X) + { + leftPoints.Add(pt); + } + else + { + rightPoints.Add(pt); + } + } + leftPoints.Sort((p, q) => p.Y.CompareTo(q.Y)); + rightPoints.Sort((p, q) => p.Y.CompareTo(q.Y)); + + var point = new PointF((leftPoints.Last().X + rightPoints.Last().X) / 2, + (leftPoints.Last().Y + rightPoints.Last().Y) / 2); + leftPoints.Add(point); + rightPoints.Add(point); + point = new PointF(point.X, bounds.Bottom); + leftPoints.Add(point); + rightPoints.Add(point); + + leftPoints.Add(new PointF(bounds.Left, bounds.Bottom)); + leftPoints.Add(new PointF(bounds.Left, bounds.Top)); + rightPoints.Add(new PointF(bounds.Right, bounds.Bottom)); + rightPoints.Add(new PointF(bounds.Right, bounds.Top)); + + point = new PointF((leftPoints.First().X + rightPoints.First().X) / 2, bounds.Top); + leftPoints.Add(point); + rightPoints.Add(point); + point = new PointF(point.X, (leftPoints.First().Y + rightPoints.First().Y) / 2); + leftPoints.Add(point); + rightPoints.Add(point); + + var path = new GraphicsPath(FillMode.Winding); + path.AddPolygon(leftPoints.ToArray()); + yield return path; + + path.Reset(); + path.AddPolygon(rightPoints.ToArray()); + yield return path; } private static GraphicsPath CreateGraphicsPath(PointF origin, PointF centerPoint, float effectiveRadius) @@ -221,20 +327,26 @@ namespace Svg { case SvgGradientSpreadMethod.Reflect: newScale = (float)Math.Ceiling(scale); - pos = (from p in colorBlend.Positions select p / newScale).ToList(); + pos = (from p in colorBlend.Positions select 1 + (p - 1) / newScale).ToList(); colors = colorBlend.Colors.ToList(); for (var i = 1; i < newScale; i++) { if (i % 2 == 1) { - pos.AddRange(from p in colorBlend.Positions.Reverse().Skip(1) select (1 - p + i) / newScale); - colors.AddRange(colorBlend.Colors.Reverse().Skip(1)); + for (int j = 1; j < colorBlend.Positions.Length; j++) + { + pos.Insert(0, (newScale - i - 1) / newScale + 1 - colorBlend.Positions[j]); + colors.Insert(0, colorBlend.Colors[j]); + } } else { - pos.AddRange(from p in colorBlend.Positions.Skip(1) select (p + i) / newScale); - colors.AddRange(colorBlend.Colors.Skip(1)); + for (int j = 0; j < colorBlend.Positions.Length - 1; j++) + { + pos.Insert(j, (newScale - i - 1) / newScale + colorBlend.Positions[j]); + colors.Insert(j, colorBlend.Colors[j]); + } } } @@ -249,19 +361,23 @@ namespace Svg for (var i = 1; i < newScale; i++) { - pos.AddRange(from p in colorBlend.Positions select (p <= 0 ? 0.001f : p) / newScale); + pos.AddRange(from p in colorBlend.Positions select (i + (p <= 0 ? 0.001f : p)) / newScale); colors.AddRange(colorBlend.Colors); } + colorBlend.Positions = pos.ToArray(); + colorBlend.Colors = colors.ToArray(); + outScale = newScale; break; default: - for (var i = 0; i < colorBlend.Positions.Length - 1; i++) - { - colorBlend.Positions[i] = 1 - (1 - colorBlend.Positions[i]) / scale; - } + outScale = 1.0f; + //for (var i = 0; i < colorBlend.Positions.Length - 1; i++) + //{ + // colorBlend.Positions[i] = 1 - (1 - colorBlend.Positions[i]) / scale; + //} - colorBlend.Positions = new[] { 0F }.Concat(colorBlend.Positions).ToArray(); - colorBlend.Colors = new[] { colorBlend.Colors.First() }.Concat(colorBlend.Colors).ToArray(); + //colorBlend.Positions = new[] { 0F }.Concat(colorBlend.Positions).ToArray(); + //colorBlend.Colors = new[] { colorBlend.Colors.First() }.Concat(colorBlend.Colors).ToArray(); break; } @@ -288,4 +404,4 @@ namespace Svg return newObj; } } -} \ No newline at end of file +} diff --git a/Source/Paths/CoordinateParser.cs b/Source/Paths/CoordinateParser.cs new file mode 100644 index 0000000..5216d23 --- /dev/null +++ b/Source/Paths/CoordinateParser.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Globalization; + +namespace Svg +{ + internal class CoordinateParser + { + private enum NumState + { + invalid, + separator, + prefix, + integer, + decPlace, + fraction, + exponent, + expPrefix, + expValue + } + + private string _coords; + private int _pos = 0; + private NumState _currState = NumState.separator; + private NumState _newState = NumState.separator; + private int i = 0; + private bool _parseWorked = true; + + public int Position { get { return _pos; } } + + public CoordinateParser(string coords) + { + _coords = coords; + if (string.IsNullOrEmpty(_coords)) _parseWorked = false; + if (char.IsLetter(coords[0])) i++; + } + + public bool HasMore { get { return _parseWorked; } } + + private bool MarkState(bool state) + { + _parseWorked = state; + i++; + return state; + } + + public bool TryGetBool(out bool result) + { + while (i < _coords.Length && _parseWorked) + { + switch (_currState) + { + case NumState.separator: + if (IsCoordSeparator(_coords[i])) + { + _newState = NumState.separator; + } + else if (_coords[i] == '0') + { + result = false; + _newState = NumState.separator; + _pos = i + 1; + return MarkState(true); + } + else if (_coords[i] == '1') + { + result = true; + _newState = NumState.separator; + _pos = i + 1; + return MarkState(true); + } + else + { + result = false; + return MarkState(false); + } + break; + default: + result = false; + return MarkState(false); + } + i++; + } + result = false; + return MarkState(false); + } + + public bool TryGetFloat(out float result) + { + while (i < _coords.Length && _parseWorked) + { + switch (_currState) + { + case NumState.separator: + if (char.IsNumber(_coords[i])) + { + _newState = NumState.integer; + } + else if (IsCoordSeparator(_coords[i])) + { + _newState = NumState.separator; + } + else + { + switch (_coords[i]) + { + case '.': + _newState = NumState.decPlace; + break; + case '+': + case '-': + _newState = NumState.prefix; + break; + default: + _newState = NumState.invalid; + break; + } + } + break; + case NumState.prefix: + if (char.IsNumber(_coords[i])) + { + _newState = NumState.integer; + } + else if (_coords[i] == '.') + { + _newState = NumState.decPlace; + } + else + { + _newState = NumState.invalid; + } + break; + case NumState.integer: + if (char.IsNumber(_coords[i])) + { + _newState = NumState.integer; + } + else if (IsCoordSeparator(_coords[i])) + { + _newState = NumState.separator; + } + else + { + switch (_coords[i]) + { + case '.': + _newState = NumState.decPlace; + break; + case 'E': + case 'e': + _newState = NumState.exponent; + break; + case '+': + case '-': + _newState = NumState.prefix; + break; + default: + _newState = NumState.invalid; + break; + } + } + break; + case NumState.decPlace: + if (char.IsNumber(_coords[i])) + { + _newState = NumState.fraction; + } + else if (IsCoordSeparator(_coords[i])) + { + _newState = NumState.separator; + } + else + { + switch (_coords[i]) + { + case 'E': + case 'e': + _newState = NumState.exponent; + break; + case '+': + case '-': + _newState = NumState.prefix; + break; + default: + _newState = NumState.invalid; + break; + } + } + break; + case NumState.fraction: + if (char.IsNumber(_coords[i])) + { + _newState = NumState.fraction; + } + else if (IsCoordSeparator(_coords[i])) + { + _newState = NumState.separator; + } + else + { + switch (_coords[i]) + { + case '.': + _newState = NumState.decPlace; + break; + case 'E': + case 'e': + _newState = NumState.exponent; + break; + case '+': + case '-': + _newState = NumState.prefix; + break; + default: + _newState = NumState.invalid; + break; + } + } + break; + case NumState.exponent: + if (char.IsNumber(_coords[i])) + { + _newState = NumState.expValue; + } + else if (IsCoordSeparator(_coords[i])) + { + _newState = NumState.invalid; + } + else + { + switch (_coords[i]) + { + case '+': + case '-': + _newState = NumState.expPrefix; + break; + default: + _newState = NumState.invalid; + break; + } + } + break; + case NumState.expPrefix: + if (char.IsNumber(_coords[i])) + { + _newState = NumState.expValue; + } + else + { + _newState = NumState.invalid; + } + break; + case NumState.expValue: + if (char.IsNumber(_coords[i])) + { + _newState = NumState.expValue; + } + else if (IsCoordSeparator(_coords[i])) + { + _newState = NumState.separator; + } + else + { + switch (_coords[i]) + { + case '.': + _newState = NumState.decPlace; + break; + case '+': + case '-': + _newState = NumState.prefix; + break; + default: + _newState = NumState.invalid; + break; + } + } + break; + } + + if (_newState < _currState) + { + result = float.Parse(_coords.Substring(_pos, i - _pos), NumberStyles.Float, CultureInfo.InvariantCulture); + _pos = i; + _currState = _newState; + return MarkState(true); + } + else if (_newState != _currState && _currState == NumState.separator) + { + _pos = i; + } + + if (_newState == NumState.invalid) + { + result = float.MinValue; + return MarkState(false); + } + _currState = _newState; + i++; + } + + if (_currState == NumState.separator || !_parseWorked || _pos >= _coords.Length) + { + result = float.MinValue; + return MarkState(false); + } + else + { + result = float.Parse(_coords.Substring(_pos, _coords.Length - _pos), NumberStyles.Float, CultureInfo.InvariantCulture); + _pos = _coords.Length; + return MarkState(true); + } + } + + private static bool IsCoordSeparator(char value) + { + switch (value) + { + case ' ': + case '\t': + case '\n': + case '\r': + case ',': + return true; + } + return false; + } + } +} diff --git a/Source/Paths/SvgPath.cs b/Source/Paths/SvgPath.cs index 25de7c2..66f0b2e 100644 --- a/Source/Paths/SvgPath.cs +++ b/Source/Paths/SvgPath.cs @@ -139,36 +139,42 @@ namespace Svg if (this.Stroke != null && this.Stroke != SvgColourServer.None) { float strokeWidth = this.StrokeWidth.ToDeviceValue(renderer, UnitRenderingType.Other, this); - using (Pen pen = new Pen(this.Stroke.GetBrush(this, renderer, this.StrokeOpacity * this.Opacity), strokeWidth)) - { - if (this.StrokeDashArray != null && this.StrokeDashArray.Count > 0) - { - /* divide by stroke width - GDI behaviour that I don't quite understand yet.*/ - pen.DashPattern = this.StrokeDashArray.ConvertAll(u => u.Value / ((strokeWidth <= 0) ? 1 : strokeWidth)).ToArray(); - } - - var path = this.Path(renderer); - renderer.DrawPath(pen, path); - - if (this.MarkerStart != null) - { - SvgMarker marker = this.OwnerDocument.GetElementById(this.MarkerStart.ToString()); - marker.RenderMarker(renderer, this, path.PathPoints[0], path.PathPoints[0], path.PathPoints[1]); - } - - if (this.MarkerMid != null) - { - SvgMarker marker = this.OwnerDocument.GetElementById(this.MarkerMid.ToString()); - for (int i = 1; i <= path.PathPoints.Length - 2; i++) - marker.RenderMarker(renderer, this, path.PathPoints[i], path.PathPoints[i - 1], path.PathPoints[i], path.PathPoints[i + 1]); - } - - if (this.MarkerEnd != null) - { - SvgMarker marker = this.OwnerDocument.GetElementById(this.MarkerEnd.ToString()); - marker.RenderMarker(renderer, this, path.PathPoints[path.PathPoints.Length - 1], path.PathPoints[path.PathPoints.Length - 2], path.PathPoints[path.PathPoints.Length - 1]); - } - } + using (var brush = this.Stroke.GetBrush(this, renderer, this.StrokeOpacity * this.Opacity)) + { + if (brush != null) + { + using (Pen pen = new Pen(brush, strokeWidth)) + { + if (this.StrokeDashArray != null && this.StrokeDashArray.Count > 0) + { + /* divide by stroke width - GDI behaviour that I don't quite understand yet.*/ + pen.DashPattern = this.StrokeDashArray.ConvertAll(u => u.Value / ((strokeWidth <= 0) ? 1 : strokeWidth)).ToArray(); + } + + var path = this.Path(renderer); + renderer.DrawPath(pen, path); + + if (this.MarkerStart != null) + { + SvgMarker marker = this.OwnerDocument.GetElementById(this.MarkerStart.ToString()); + marker.RenderMarker(renderer, this, path.PathPoints[0], path.PathPoints[0], path.PathPoints[1]); + } + + if (this.MarkerMid != null) + { + SvgMarker marker = this.OwnerDocument.GetElementById(this.MarkerMid.ToString()); + for (int i = 1; i <= path.PathPoints.Length - 2; i++) + marker.RenderMarker(renderer, this, path.PathPoints[i], path.PathPoints[i - 1], path.PathPoints[i], path.PathPoints[i + 1]); + } + + if (this.MarkerEnd != null) + { + SvgMarker marker = this.OwnerDocument.GetElementById(this.MarkerEnd.ToString()); + marker.RenderMarker(renderer, this, path.PathPoints[path.PathPoints.Length - 1], path.PathPoints[path.PathPoints.Length - 2], path.PathPoints[path.PathPoints.Length - 1]); + } + } + } + } } } diff --git a/Source/Paths/SvgPathBuilder.cs b/Source/Paths/SvgPathBuilder.cs index 2bd64fe..91abb2f 100644 --- a/Source/Paths/SvgPathBuilder.cs +++ b/Source/Paths/SvgPathBuilder.cs @@ -284,322 +284,7 @@ namespace Svg } } - private enum NumState - { - invalid, - separator, - prefix, - integer, - decPlace, - fraction, - exponent, - expPrefix, - expValue - } - - private class CoordinateParser - { - private string _coords; - private int _pos = 0; - private NumState _currState = NumState.separator; - private NumState _newState = NumState.separator; - private int i = 1; - private bool _parseWorked = true; - - public CoordinateParser(string coords) - { - _coords = coords; - } - - public bool HasMore { get { return _parseWorked; } } - - private bool MarkState(bool state) - { - _parseWorked = state; - i++; - return state; - } - - public bool TryGetBool(out bool result) - { - while (i < _coords.Length && _parseWorked) - { - switch (_currState) - { - case NumState.separator: - if (IsCoordSeparator(_coords[i])) - { - _newState = NumState.separator; - } - else if (_coords[i] == '0') - { - result = false; - _newState = NumState.separator; - _pos = i + 1; - return MarkState(true); - } - else if (_coords[i] == '1') - { - result = true; - _newState = NumState.separator; - _pos = i + 1; - return MarkState(true); - } - else - { - result = false; - return MarkState(false); - } - break; - default: - result = false; - return MarkState(false); - } - i++; - } - result = false; - return MarkState(false); - } - - public bool TryGetFloat(out float result) - { - while (i < _coords.Length && _parseWorked) - { - switch (_currState) - { - case NumState.separator: - if (char.IsNumber(_coords[i])) - { - _newState = NumState.integer; - } - else if (IsCoordSeparator(_coords[i])) - { - _newState = NumState.separator; - } - else - { - switch (_coords[i]) - { - case '.': - _newState = NumState.decPlace; - break; - case '+': - case '-': - _newState = NumState.prefix; - break; - default: - _newState = NumState.invalid; - break; - } - } - break; - case NumState.prefix: - if (char.IsNumber(_coords[i])) - { - _newState = NumState.integer; - } - else if (_coords[i] == '.') - { - _newState = NumState.decPlace; - } - else - { - _newState = NumState.invalid; - } - break; - case NumState.integer: - if (char.IsNumber(_coords[i])) - { - _newState = NumState.integer; - } - else if (IsCoordSeparator(_coords[i])) - { - _newState = NumState.separator; - } - else - { - switch (_coords[i]) - { - case '.': - _newState = NumState.decPlace; - break; - case 'e': - _newState = NumState.exponent; - break; - case '+': - case '-': - _newState = NumState.prefix; - break; - default: - _newState = NumState.invalid; - break; - } - } - break; - case NumState.decPlace: - if (char.IsNumber(_coords[i])) - { - _newState = NumState.fraction; - } - else if (IsCoordSeparator(_coords[i])) - { - _newState = NumState.separator; - } - else - { - switch (_coords[i]) - { - case 'e': - _newState = NumState.exponent; - break; - case '+': - case '-': - _newState = NumState.prefix; - break; - default: - _newState = NumState.invalid; - break; - } - } - break; - case NumState.fraction: - if (char.IsNumber(_coords[i])) - { - _newState = NumState.fraction; - } - else if (IsCoordSeparator(_coords[i])) - { - _newState = NumState.separator; - } - else - { - switch (_coords[i]) - { - case '.': - _newState = NumState.decPlace; - break; - case 'e': - _newState = NumState.exponent; - break; - case '+': - case '-': - _newState = NumState.prefix; - break; - default: - _newState = NumState.invalid; - break; - } - } - break; - case NumState.exponent: - if (char.IsNumber(_coords[i])) - { - _newState = NumState.expValue; - } - else if (IsCoordSeparator(_coords[i])) - { - _newState = NumState.invalid; - } - else - { - switch (_coords[i]) - { - case '+': - case '-': - _newState = NumState.expPrefix; - break; - default: - _newState = NumState.invalid; - break; - } - } - break; - case NumState.expPrefix: - if (char.IsNumber(_coords[i])) - { - _newState = NumState.expValue; - } - else - { - _newState = NumState.invalid; - } - break; - case NumState.expValue: - if (char.IsNumber(_coords[i])) - { - _newState = NumState.expValue; - } - else if (IsCoordSeparator(_coords[i])) - { - _newState = NumState.separator; - } - else - { - switch (_coords[i]) - { - case '.': - _newState = NumState.decPlace; - break; - case '+': - case '-': - _newState = NumState.prefix; - break; - default: - _newState = NumState.invalid; - break; - } - } - break; - } - - if (_newState < _currState) - { - result = float.Parse(_coords.Substring(_pos, i - _pos), NumberStyles.Float, CultureInfo.InvariantCulture); - _pos = i; - _currState = _newState; - return MarkState(true); - } - else if (_newState != _currState && _currState == NumState.separator) - { - _pos = i; - } - - if (_newState == NumState.invalid) - { - result = float.MinValue; - return MarkState(false); - } - _currState = _newState; - i++; - } - - if (_currState == NumState.separator || !_parseWorked || _pos >= _coords.Length) - { - result = float.MinValue; - return MarkState(false); - } - else - { - result = float.Parse(_coords.Substring(_pos, _coords.Length - _pos), NumberStyles.Float, CultureInfo.InvariantCulture); - _pos = _coords.Length; - return MarkState(true); - } - } - - private static bool IsCoordSeparator(char value) - { - switch (value) - { - case ' ': - case '\t': - case '\n': - case '\r': - case ',': - return true; - } - return false; - } - } + //private static IEnumerable ParseCoordinates(string coords) //{ diff --git a/Source/Svg.csproj b/Source/Svg.csproj index 56a1bb4..946db35 100644 --- a/Source/Svg.csproj +++ b/Source/Svg.csproj @@ -100,6 +100,7 @@ + @@ -108,6 +109,8 @@ + + diff --git a/Source/SvgAttributeCollection.cs b/Source/SvgAttributeCollection.cs index 39e85f6..ac09091 100644 --- a/Source/SvgAttributeCollection.cs +++ b/Source/SvgAttributeCollection.cs @@ -90,7 +90,9 @@ namespace Svg (value is SvgTextAnchor && (SvgTextAnchor)value == SvgTextAnchor.inherit) || (value is SvgFontVariant && (SvgFontVariant)value == SvgFontVariant.inherit) || (value is SvgTextDecoration && (SvgTextDecoration)value == SvgTextDecoration.inherit) || - (value is XmlSpaceHandling && (XmlSpaceHandling)value == XmlSpaceHandling.inherit) || + (value is XmlSpaceHandling && (XmlSpaceHandling)value == XmlSpaceHandling.inherit) || + (value is SvgOverflow && (SvgOverflow)value == SvgOverflow.inherit) || + (value == SvgColourServer.Inherit) || (value is string && (string)value == "inherit") ); } diff --git a/Source/SvgDocument.cs b/Source/SvgDocument.cs index 1d3d0b2..67ae458 100644 --- a/Source/SvgDocument.cs +++ b/Source/SvgDocument.cs @@ -435,7 +435,7 @@ namespace Svg //Trace.TraceInformation("Begin Render"); var size = GetDimensions(); - var bitmap = new Bitmap((int)Math.Ceiling(size.Width), (int)Math.Ceiling(size.Height)); + var bitmap = new Bitmap((int)Math.Round(size.Width), (int)Math.Round(size.Height)); // bitmap.SetResolution(300, 300); try { diff --git a/Source/SvgElementIdManager.cs b/Source/SvgElementIdManager.cs index a4d10b7..2fab1c2 100644 --- a/Source/SvgElementIdManager.cs +++ b/Source/SvgElementIdManager.cs @@ -41,6 +41,7 @@ namespace Svg public virtual SvgElement GetElementById(Uri uri) { + if (uri.ToString().StartsWith("url(")) uri = new Uri(uri.ToString().Substring(4).TrimEnd(')'), UriKind.Relative); if (!uri.IsAbsoluteUri && this._document.BaseUri != null && !uri.ToString().StartsWith("#")) { var fullUri = new Uri(this._document.BaseUri, uri); diff --git a/Tests/SvgW3CTestRunner/View.cs b/Tests/SvgW3CTestRunner/View.cs index f34a3a9..59c0233 100644 --- a/Tests/SvgW3CTestRunner/View.cs +++ b/Tests/SvgW3CTestRunner/View.cs @@ -18,18 +18,24 @@ namespace SvgW3CTestRunner { InitializeComponent(); // ignore tests pertaining to javascript or xml reading - var files = (from f in (from g in Directory.GetFiles(_svgBasePath) + var passes = File.ReadAllLines(_svgBasePath + @"..\PassingTests.txt").ToDictionary((f) => f, (f) => true); + var files = (from f in + (from g in Directory.GetFiles(_svgBasePath) select Path.GetFileName(g)) where !f.StartsWith("animate-") && !f.StartsWith("conform-viewer") && - !f.Contains("-dom-") && !f.StartsWith("linking-") && !f.StartsWith("interact-") + !f.Contains("-dom-") && !f.StartsWith("linking-") && !f.StartsWith("interact-") && + !f.StartsWith("script-") orderby f select (object)f); + files = files.Where((f) => !passes.ContainsKey((string)f)).Union(Enumerable.Repeat((object)"## PASSING ##", 1)).Union(files.Where((f) => passes.ContainsKey((string)f))); + lstFiles.Items.AddRange(files.ToArray()); } private void lstFiles_SelectedIndexChanged(object sender, EventArgs e) { var fileName = lstFiles.SelectedItem.ToString(); + if (fileName.StartsWith("#")) return; try { Debug.Print(fileName); diff --git a/Tests/W3CTestSuite/PassingTests.txt b/Tests/W3CTestSuite/PassingTests.txt new file mode 100644 index 0000000..fac06ce --- /dev/null +++ b/Tests/W3CTestSuite/PassingTests.txt @@ -0,0 +1,159 @@ +color-prof-01-f.svg +color-prop-01-b.svg +color-prop-02-f.svg +color-prop-03-t.svg +color-prop-04-t.svg +color-prop-05-t.svg +coords-coord-01-t.svg +coords-coord-02-t.svg +coords-trans-01-b.svg +coords-trans-02-t.svg +coords-trans-03-t.svg +coords-trans-04-t.svg +coords-trans-05-t.svg +coords-trans-06-t.svg +coords-trans-07-t.svg +coords-trans-08-t.svg +coords-trans-09-t.svg +coords-trans-10-f.svg +coords-trans-11-f.svg +coords-trans-12-f.svg +coords-trans-13-f.svg +coords-trans-14-f.svg +coords-transformattr-01-f.svg +coords-transformattr-02-f.svg +coords-transformattr-03-f.svg +coords-transformattr-04-f.svg +coords-transformattr-05-f.svg +coords-units-01-b.svg +coords-units-02-b.svg +coords-units-03-b.svg +coords-viewattr-01-b.svg +coords-viewattr-02-b.svg +coords-viewattr-03-b.svg +masking-path-01-b.svg +masking-path-02-b.svg +masking-path-04-b.svg +masking-path-05-f.svg +masking-path-06-b.svg +masking-path-03-b.svg +pservers-grad-01-b.svg +pservers-grad-02-b.svg +pservers-grad-03-b.svg +pservers-grad-04-b.svg +pservers-grad-06-b.svg +pservers-grad-07-b.svg +pservers-grad-08-b.svg +pservers-grad-09-b.svg +pservers-grad-10-b.svg +pservers-grad-11-b.svg +pservers-grad-12-b.svg +pservers-grad-14-b.svg +pservers-grad-15-b.svg +pservers-grad-16-b.svg +pservers-grad-17-b.svg +pservers-grad-18-b.svg +pservers-grad-20-b.svg +pservers-grad-22-b.svg +pservers-grad-23-f.svg +pservers-grad-24-f.svg +pservers-grad-stops-01-f.svg +pservers-pattern-01-b.svg +pservers-pattern-02-f.svg +pservers-pattern-03-f.svg +pservers-pattern-04-f.svg +pservers-pattern-05-f.svg +pservers-pattern-06-f.svg +pservers-pattern-07-f.svg +pservers-pattern-08-f.svg +pservers-pattern-09-f.svg +render-elems-01-t.svg +render-elems-02-t.svg +render-elems-03-t.svg +render-elems-06-t.svg +render-elems-07-t.svg +render-elems-08-t.svg +render-groups-03-t.svg +shapes-circle-01-t.svg +shapes-circle-02-t.svg +shapes-ellipse-01-t.svg +shapes-ellipse-02-t.svg +shapes-ellipse-03-f.svg +shapes-grammar-01-f.svg +shapes-intro-01-t.svg +shapes-intro-02-f.svg +shapes-line-01-t.svg +shapes-line-02-f.svg +shapes-polygon-01-t.svg +shapes-polygon-02-t.svg +shapes-polygon-03-t.svg +shapes-polyline-01-t.svg +shapes-polyline-02-t.svg +shapes-rect-01-t.svg +shapes-rect-02-t.svg +shapes-rect-03-t.svg +shapes-rect-04-f.svg +shapes-rect-06-f.svg +shapes-rect-07-f.svg +struct-frag-06-t.svg +struct-group-01-t.svg +struct-group-02-b.svg +paths-data-01-t.svg +paths-data-02-t.svg +paths-data-03-f.svg +paths-data-04-t.svg +paths-data-05-t.svg +paths-data-06-t.svg +paths-data-07-t.svg +paths-data-08-t.svg +paths-data-09-t.svg +paths-data-12-t.svg +paths-data-13-t.svg +paths-data-14-t.svg +paths-data-15-t.svg +paths-data-17-f.svg +paths-data-18-f.svg +paths-data-19-f.svg +paths-data-20-f.svg +struct-image-01-t.svg +struct-image-04-t.svg +struct-image-06-t.svg +masking-path-13-f.svg +masking-path-14-f.svg +painting-control-01-f.svg +painting-control-03-f.svg +painting-fill-01-t.svg +painting-fill-02-t.svg +painting-fill-03-t.svg +painting-fill-04-t.svg +painting-fill-05-b.svg +painting-stroke-01-t.svg +struct-use-05-b.svg +styling-class-01-f.svg +styling-css-01-b.svg +styling-css-03-b.svg +styling-css-07-f.svg +text-align-01-b.svg +text-align-02-b.svg +text-align-03-b.svg +text-fonts-01-t.svg +text-fonts-02-t.svg +text-fonts-03-t.svg +text-fonts-05-f.svg +text-intro-04-t.svg +text-intro-06-t.svg +text-path-01-b.svg +text-path-02-b.svg +text-spacing-01-b.svg +text-text-01-b.svg +text-text-04-t.svg +text-text-05-t.svg +text-text-07-t.svg +text-text-09-t.svg +text-text-10-t.svg +text-text-11-t.svg +text-tref-01-b.svg +text-tspan-01-b.svg +text-ws-01-t.svg +text-ws-02-t.svg +text-ws-03-t.svg \ No newline at end of file -- GitLab