Commit 3f4ee333 authored by Eric Domke's avatar Eric Domke
Browse files

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
parent 9e878812
......@@ -13,9 +13,7 @@ namespace Svg
public class SvgGradientStop : SvgElement
{
private SvgUnit _offset;
private SvgPaintServer _colour;
private float _opacity;
/// <summary>
/// Gets or sets the offset, i.e. where the stop begins from the beginning, of the gradient stop.
/// </summary>
......@@ -59,20 +57,31 @@ namespace Svg
/// </summary>
[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<SvgPaintServer>("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; }
}
/// <summary>
/// Gets or sets the opacity of the gradient stop (0-1).
/// </summary>
[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);
}
/// <summary>
......@@ -81,8 +90,6 @@ namespace Svg
public SvgGradientStop()
{
this._offset = new SvgUnit(0.0f);
this._colour = SvgColourServer.NotSet;
this._opacity = 1.0f;
}
/// <summary>
......@@ -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<SvgColourServer>(_colour, parent);
var core = SvgDeferredPaintServer.TryGet<SvgColourServer>(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<T>() as SvgGradientStop;
newObj.Offset = this.Offset;
newObj.Colour = this.Colour;
newObj.Opacity = this.Opacity;
return newObj;
}
}
......
This diff is collapsed.
......@@ -13,6 +13,8 @@ namespace Svg
[TypeConverter(typeof(SvgPaintServerFactory))]
public abstract class SvgPaintServer : SvgElement
{
public Func<SvgPaintServer> GetCallback { get; set; }
/// <summary>
/// An unspecified <see cref="SvgPaintServer"/>.
/// </summary>
......@@ -39,7 +41,7 @@ namespace Svg
/// </summary>
/// <param name="styleOwner">The owner <see cref="SvgVisualElement"/>.</param>
/// <param name="opacity">The opacity of the brush.</param>
public abstract Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity);
public abstract Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity, bool forStroke = false);
/// <summary>
/// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
......
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<SvgPaintServer>();
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)
......
......@@ -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
/// <summary>
/// Gets or sets the width of the pattern.
/// </summary>
[SvgAttribute("patternUnits")]
[SvgAttribute("patternContentUnits")]
public SvgCoordinateUnits PatternContentUnits
{
get { return this._patternContentUnits; }
......@@ -113,15 +115,56 @@ namespace Svg
set { this._y = value; }
}
/// <summary>
/// Gets or sets another gradient fill from which to inherit the stops from.
/// </summary>
[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<SvgTransformCollection>("gradientTransform")); }
set { this.Attributes["gradientTransform"] = value; }
}
protected Matrix EffectivePatternTransform
{
get
{
var transform = new Matrix();
if (PatternTransform != null)
{
transform.Multiply(PatternTransform.GetMatrix());
}
return transform;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="SvgPatternServer"/> class.
/// </summary>
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);
}
/// <summary>
......@@ -129,57 +172,78 @@ namespace Svg
/// </summary>
/// <param name="renderingElement">The owner <see cref="SvgVisualElement"/>.</param>
/// <param name="opacity">The opacity of the brush.</param>
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<SvgPatternServer>();
var curr = this;
while (curr != null)
{
chain.Add(curr);
curr = SvgDeferredPaintServer.TryGet<SvgPatternServer>(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<SvgPatternServer>();
......
......@@ -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<GraphicsPath> 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<PointF>();
var rightPoints = new List<PointF>();
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
}
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;
}
}
}
......@@ -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<SvgMarker>(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<SvgMarker>(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<SvgMarker>(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<SvgMarker>(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<SvgMarker>(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<SvgMarker>(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]);
}
}
}
}
}
}
......
......@@ -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<float> ParseCoordinates(string coords)
//{
......
......@@ -100,6 +100,7 @@
<Compile Include="Clipping and Masking\SvgClipPath.cs" />
<Compile Include="Clipping and Masking\SvgMask.cs" />
<Compile Include="DataTypes\ISvgSupportsCoordinateUnits.cs" />
<Compile Include="DataTypes\SvgPointCollection.cs" />
<Compile Include="DataTypes\SvgTextDecoration.cs" />
<Compile Include="DataTypes\SvgTextLengthAdjust.cs" />
<Compile Include="DataTypes\SvgTextPathMethod.cs" />
......@@ -108,6 +109,8 @@
<Compile Include="Document Structure\SvgSymbol.cs" />
<Compile Include="Filter Effects\ImageBuffer.cs" />
<Compile Include="Painting\GenericBoundable.cs" />
<Compile Include="Painting\SvgFallbackPaintServer .cs" />
<Compile Include="Paths\CoordinateParser.cs" />
<Compile Include="Rendering\IGraphicsProvider.cs" />
<Compile Include="Rendering\ISvgRenderer.cs" />
<Compile Include="SvgNodeReader.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")
);
}
......
......@@ -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
{
......
......@@ -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);
......
......@@ -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);
......
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
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment